text:
Capital markets: Litigation funding finds its feet
By Elliot Wilson
Litigation funding has surged in recent years. The asset class is catnip to yield-hungry investors, with funders expanding from their roots in Australia and the UK to tap new markets from Germany to Brazil and the US.
Nick Rowles-Davies has been called a few things in his career, but the one that comes directly to mind involves a throwaway line by a member of the Fourth Estate.
"I was once described by a journalist as being a 'pinstriped ambulance chaser'," he says. "I took exception to that. I have never owned a pinstriped suit."
The choice of insult makes sense once you know what he does. Rowles-Davies, a cheery Englishman, is the executive vice-chairman of Sydney-based LCM Finance, one of the world's oldest litigation finance companies.
Also known as litigation funding, it is an uncorrelated asset class that's been around for a few decades, growing at a steady pace without ever quite catching fire. But given what it offers – outsized returns at a time of super-low yields on judiciously placed investments – is this its moment in the sun?
At its heart, litigation finance is the process of investing in or buying up – both terms work just as well – outstanding legal claims, then working to profit from them through settlement out of court or adjudication in it. Hence the 'ambulance chaser' jibe.
You have reached premium content. Please log in to continue reading.
Read beyond the headlines with Euromoney
For over 50 years, our readers have looked to Euromoney to stay informed about the issues that matter in the international banking and financial markets. Find out more about our different levels of access below.
SUBSCRIBE ONLINE TODAY
Unlimited access to Euromoney.com and Asiamoney.com
Expert comment, long reads and in-depth analysis interviews with senior finance professionals
Access the results of our market-leading annual surveys across core financial services
Access the results of our annual awards, including the world-renowned Awards for Excellence
Your print copy of Euromoney magazine delivered monthly
FREE 7 DAY TRIAL
Unlimited access to Euromoney.com and Asiamoney.com, including our top stories, long reads, expert analysis, and the results of our annual surveys and awards
Sign up to any of our newsletters, curated by our editors
Capital Markets Capital MarketsNorth AmericaWestern EuropeFeatures
Elliot Wilson
Elliot Wilson is Greater China Editor and Private Banking and Wealth Management Editor. He joined the magazine in 2020 having been a regular contributor focusing on China and the Indian subcontinent, Russia and Eastern Europe/the CIS. He is based in Hong Kong.
Where will it end? Now Spacs might start buying listed companies
Mark Baker, January 18, 2021
The chart that worried Toshiba's shareholders
Chris Wright, January 18, 2021
Ant's miserable Christmas and unhappy new year
Elliot Wilson, January 04, 2021
The bondholder treatment at the heart of the Petropavlovsk brawl
Chris Wright, December 18, 2020
Societe Generale details equity derivatives cuts
Dominic O'Neill, December 17, 2020


text:
Fontainebleau State Park (2009)
Place Covered: Mandeville, St. Tammany Parish, Louisiana, 30.3376496, -90.0403297
Subjects: Louisiana. Office of State Parks | Fontainebleau State Park | Native Americans | Louisiana state parks | Nature | Sugar factories | Marigny, Bernard, 1785-1868 | Tchefuncte culture | Prehistoric peoples | LAKE PONTCHARTRAIN
Whinham, Charlie Producer
Fortenberry, Rex Q. Photographer
Borel, Dwayne Interviewee
Jones, Frank "Skipper" Interviewee
Scott, Richard Interviewee
Perkins, Grey Hawk Interviewee
Sanford, Shauna Host
This segment from the June 5, 2009, episode of the series "Louisiana: The State We're In" features Charlie Whinham's visit to Fontainebleau State Park, a 2,800 acre site located on the shore of Lake Pontchartrain in Mandeville. Whinham tours the site and talks to park manager Dwayne Borel, assistant manager Frank "Skipper" Jones, and interpretive ranger Richard Scott about the park's main attractions, including a beach, boardwalk, campsites, cabins, the Tammany Trace nature trail, and a crumbling sugar mill built in 1829 by Bernard de Marigny de Mandeville, the founder of Mandeville and previous owner of the land. He also talks to park employee Grey Hawk Perkins about the history of the Tchefuncte culture and Native Americans on the land dating back 2,000 years ago. This story is a part of the "Louisiana's Natural Treasures: State Parks" series. Hosts: Charlie Whinham and Shauna Sanford
Louisiana's Natural Treasures

redpajama_set_name: RedPajamaCommonCrawl


In what his arguably an album that represents his most mature and noted evolution as a composer, performer and collaborator to date, Trippin', the newest record released under Robert Miller's Project Grand Slam brand, is not just a watershed release for the artist behind its creation and design, but also the genre that it seeks to revive. Jazz fusion suffered some pretty big commercial blows in the last fifteen or twenty years, but a renewed sense of experimentalism within the indie pop/rock world has led to an increased interest in artists like Robert Miller and his eclectic approach to recording. One needs to look no further than "Lament," the star track from Trippin', to understand why Project Grand Slam is quickly becoming one of the most talked about jazz bands in all of underground or mainstream music today.
Everybody has been making throwback records lately. It's sort of like the sad phenomenon in Hollywood has spread into the recording industry; tons of reboots and remakes are flooding the market and overshadowing inventive, fresh releases from artists who aren't looking to recreate some sound from the past. I really love classic movies and albums, we all do, but why is it that so many of the smartest voices in entertainment are making the indubitably asinine decision to draw from a well that's been out of date for decades now? Have they really run out of new ideas? That's nothing short of laughable when you listen to "Lament," a song so willfully experimental and ambitiously delivered in crystal clear, colorful high definition audio for us to behold all of its glorious splendor and vivaciousness. Project Grand Slam didn't get into the studio and decide to make a smooth jazz record from the 1990's, because that would have been, well, stupid to say the least. They picked up their instruments and dove into progressive postmodernism, embraced the noise, and yielded an ambient pop/jazz track that isn't like anything else I've listened to or will listen to as a music journalist or as a fan. That's true professionalism right there.
While I would have possibly altered the final mix, just slightly, so that guitarist Tristan Clark's string sections of "Lament" were a little more jubilant and visible beside Ziarra Washington's earth shaking vocals, there's really nothing to complain about with this song or the excellent album it comes from. I won't even mince words; this is probably my favorite song of the year, and it's for one really basic, fundamental reason; it's different. It bravely tries things that other songs aren't. It doesn't sound like these musicians are holding back, nothing sounds like an imitation or an altered perspective from what the composer was originally driving at. This is an opulent collection of original, straight up melodies that mesh perfectly with each other. It's indulgent and yet somewhat modest at the same time. Aesthetically contradictory and thought provoking. I'm in love with "Lament" and the direction that Project Grand Slam are taking their music in, and once this track gets into standard rotation on specialty radio later this year, I'm positive I won't be the only one.


" "They call him the Rabbi!"
The Rabbi is one of the two main antagonists of the movie Lucky Number Slevin.
He was portrayed by Academy Award-winning actor, Sir Ben Kingsley, who also portrayed Nizam, Archibald Snatcher, and Don Logan.
Twenty years before the main events of the movie, the Rabbi was the business partner of The Boss. They made money by fixing horse races and when a man called Max accidentally gets wind of the manipulation and sets a lot of money on the horse supposed to win, the two decide to set an example. They have Max ambushed when he returns to his car. Max is kidnapped and tortured by the Rabbi and the Boss. When Max begs to be returned to his family, the two reveal that Max' family is already dead. Indeed, one of their henchmen headed home to Max' house and killed his wife but Goodkat, the killer hired to murder Max' son, secretly left the boy alive and took him in on his own.
Eventually, the Boss and the Rabbi part ways and even become rivals. Both build up a criminal empire but - out of fear of being eliminated by the other - both never leave their penthouse apartments ever again. Both men have their headquarters in penthouse apartments on the same street and are able to watch the other man's office window from their own office.
Unbeknownst to the Rabbi and the Boss, Goodkat raised the spared boy as his own son and the two plotted revenge on the two mob bosses. To set off the events, the boy - now known as Slevin - killed the Bosses son. The Boss believes that this was done by the Rabbi and decides that the Rabbi's son must also be killed. He has a guy who owes him a lot of money brought to his office to do the deed. He is unaware that this guy's identity has been taken by Slevin.
Moments after he has been returned to his apartment, Slevin is picked up by two of the Rabbi's men who deliver him to the Rabbi. The Rabbi, who also believes that Slevin is the person whose identity he took on, demands that Slevin pays him back the money "Nick" owes him. He soon realizes that Slevin is not Nick but demands the money anyhow as to not endanger a deal he has with Goodkat, who is manipulating both the Rabbi and the Boss from behind the scenes. Moments before he leaves the Rabbi's office, Slevin turns around to ask him a question but the Rabbi assumes that Slevin wants to attack him and swiftly draws a shotgun from under his table. This shotgun, which the Rabbi used twenty years ago, confirms to Slevin that the Rabbi is indeed the person he is looking for.
Together with Goodkat, Slevin murders the Rabbi's son - seemingly because he was forced by the Boss. However, Slevin then returns to the Rabbi, who is not yet aware of the death of his son and hands him a suitcase, claiming that the Rabbi's money is inside. However, when the Rabbi opens the suitcase, he is knocked out by Slevin with a blow to the head.
When the Rabbi wakes up, he finds himself strapped to a chair in the penthouse of the Boss, which he recognizes from back when they were partners. To his surprise, the Boss is tied to a chair as well; both men are sitting with the back to the other. Once the two recognize their old partners sitting behind them, they blame each other for the attacks on their businesses, unaware that the attacks were actually carried out by Slevin and Goodkat. During this conversation, the Boss spitefully tells the Rabbi that he had his son executed.
The conversation is disrupted when Slevin arrives in the room. The Rabbi tries to bribe Slevin because he expects that Slevin is working for somebody else. Slevin, however, reveals that he cannot be bribed because everything he did was his own plan. He reveals his entire plan and backstory to them and thus reveals that they are responsible for the death of his family. He then kills the two men the same way they killed his father - by putting a plastic bag over their heads and taping it shut, suffocating the two to death and avenging his parents.


On an area of around 5,000 m², eagles, buzzards, hawks and kites make their rounds in daily flight demonstrations Around 25 to 30 rare birds of prey live here in pairs in the aviary all year round, a species-appropriate attitude is our top priority. Cared and trained by professional falconers, birds of prey are the undisputed stars in the flight demonstrations. In elegant gliding, daredevil dive's, fast-paced gliding flight, they glide over the heads of the spectators at breathtaking speed or silently glide towards prey or the outstretched hand of the respective falconer. An impressive experience for the whole family..An educational trail on the history of falconry completes the offer. You can visit the birds in the aviaries, half an hour before and after the flight demonstrations.
Many thanks to all of our Birds of Prey sponsors! Your contribution is extremly valuable and enables our birds the best possible care!
Many imposing species of birds of prey such as eagles, vultures kites, buzzards, hawks, ravens and owls find themselves at home on the Birds of Prey Park in Umhausen.
The Birds of Prey Park in Umhausen is open daily from the end of April till the beginning of November. Off season: Mondays closed.
The spacious arena seats 400 visitors and offers the perfect place to watch the flight shows and experiance the birds up close!
Falconry belongs to the intangible cultural heritage of UNESCO. The nature trail in the Birds of Prey Park informs about the traing of falcons.
The Birds of Prey Park offers wheelchair and push-chair access on the whole grounds.
You can view the birds about half an hour before and after the shows.
The aim of the 'Birds of Prey Park' is also to arouse the interest and understanding of endangered species, to keep conversation's about our environment alive, and to bring the visitor closer. The promotion and conservation of biological diversity, educational work, pedagogical activities for children and the idea of, protection are at the centre of our work. For the keeping and care of our birds we have particularly strict conditions, which are observed for the benefit of the animals.
The stone sculpture "Steinadler" has landed!
A work from artist Werner Pienz – Art with soul and style!


text:
Q: How To show ALL text as a default on search in LWC I have one datalist where I am getting all account Names and displaying like a dropdown.
I want to add --ALL-- as a default option
Like currently my compo is like

but I want to add --ALL-- As a default option like below

 <label class="slds-form-element__label" style="margin-right: 0.5rem;">Select Account</label>
 <input data-id="inputData" onchange={} required value={accuntName} name="input" list="filterList" placeholder="search Account" class="slds-input" type="text" autocomplete="off" />
    <datalist data-id="AccountList" id="AccountList" class="">
        <template for:each={accountDeails} for:item='item'>
            <option key={item} value={item}>{item}</option>
        </template>
    </datalist>

JS
accountDeails()
    .then(result=>{
     if(result){

        this.accountDeails = result;
    }
let listAcc= this.template.querySelector('datalist[data-id="AccountList"]').id;
this.template.querySelector('input[data-id="inputData"]').setAttribute("list", listAcc);

Apex
@auraEnabled
public static list<string> accountDeails() {
list<account>accList =[select id from account];
list<string> acName = new list<string>();
for(account ac:accList) {
acName.add(ac.Name);
}
return acName;

A: The idea is that this value should be added to the datalist options and as selected value for the input.
You can do it in Apex
@AuraEnabled
public static List<String> accountDeails() {
    List<Account> accList =[select Id from Account];
    List<String> acName = new List<String>{'--ALL--'};
    for(Account ac : accList) {
        acName.add(ac.Name);
    }
return acName;

in JS set accuntName to --ALL--
....
accountDeails()
    .then(result=>{
        if(result){
            this.accountDeails = result;
            this.accuntName = '--ALL--';
        }
....

A: You can simply add it in your template, if you want to:
<datalist data-id="AccountList" id="AccountList" class="">
  <option value="">-- ALL --</option>
  <option for:each={accountDeails} for:item='item' key={item} value={item}>{item}</option>
</datalist>

Note that using a template isn't required for for:each if you don't want to use it (but it's fine if you do).

redpajama_set_name: RedPajamaStackExchange


Take time to find a great web designer or learn as much as you can about creating a great site yourself. If you can write, cash in on this skill by crafting speeches that make birthdays, weddings, award ceremonies, or political debates a more memorable experience. Dropshipping Dropshipping is the easiest option to start an ecommerce business without having to buy any products in advance. To start I love its name! Have stunning photography Your design work is going to live and die by its photography. Ghostwriter If writing is your thing but you are lacking a topic to write about become a ghostwriter for someone else.
What is the standard format for a management consulting presentation?
Become a travel consultant to help adventure-seekers plan their unforgettable holidays from A to Z and enjoy every second of your working day. This apprentice will work to complete objectives, grow readership and revenues, and generate profitable results for the business. The application process each step is mandatory The goal is for this apprenticeship to last for 12 months, starting in August of 2017. Travel consultant Do you spend more time packing and unpacking your luggage than most of the people you know? Now take that same dozen and double it. This completely mobile business doesn't require any special equipment; you only need a space to park your truck when it's not in use. Focus on these efforts most. Because finding furniture and finishes that look great on budget takes so much time that even though you might be able to work within their budget you will not be factoring in your time being paid for and that my friends, means you are working for free and therefore do not have a business.
And finally, Perrin Carrell found himself the proud owner of a new adopted puppy. It all makes me feel as though I am a white elephant, costly and useless to all. Getting up earlier isn't one of them. Ideally, we will work together until you feel there is nothing left for you to learn. It worked well and was a fun little side hustle experiment. If you have an Apple product, you can use to manage this. Become an interior design consultant and have people send you pictures, ideas, and dimensions to their rooms for you to decorate digitally for them.
I want to teach you everything I know about copywriting. Instead of selling every product imaginable, they only sell dance clothing. It to finish this type of project so be sure to have a few months free to give to it. I hope this has given you some useful information! Feature these shots on your instagram and Pinterest accounts. You can use the likes of to pick furniture from real stores and choose a color scheme that suits the owner. But you will gain knowledge of how to generate wealth for yourself, start your own business, and own your work freedom.
So he started the Mobile App Chat podcast now called to talk to other more successful app developers and get their advice. If this sounds interesting to you, here are 16 mobile businesses you can explore further. Your Turn Which of these generating methods could you use? Flag Abuse Flagging a post will send it to the Goodreads Customer Care team for review. In the first, list your skills. You may be able to do it for a year or so but you'll soon realise it's not sustainable. They will know all of the 'on trend' angles to shoot it magazine style, and they will be able to light it and edit the photos to make your work look the absolute best it can.
Translator If you are gifted a more than one language but have no interest in teaching others, try translating. How do you think that would impact you psychologically? The Consulting Apprenticeship is a forthcoming book written for business professionals and consultants with a focus on nuances passed on during apprenticeship regarding the execution of consulting-style projects. Now, push the first domino and what happens? Push the first domino and what happens? It was one of the best decisions I could have made for my business. This is a huge time-saver for busy people, often making it an easy sell. Locksmiths do most of their work outside of a traditional location, helping those who are locked out, and installing and repairing locks on doors, windows, safes, and cars, making it the perfect mobile business. You are getting the opportunity to work with a world class marketer.
Look for ways to use your existing talents in lines of work that you would find personally rewarding. Most people already sleep to little. Be intentional with your actions. For many people, when they start exercising, they stop using their credit cards quite so often. These posts will give you a deep understanding of how these platforms work, and give you knowledge by osmosis. Good opportunity to correlate this with your sleep time from the previous section. Most office-based jobs can now be done online through or remote working.
So he thought, if none of the existing dog blogs have truly exceptional content, maybe I could build one to fill the void. They tried to include every product under the sun, and as a result, left opportunity for a sniper like me. The short answer is: Who gives a flying F about qualifications. If successful, all it takes to expand is a second truck and a few more helpers. Ethical fashion brand One thing is clear — clothes and accessories will always be one of the super-profitable online business ideas for an ecommerce site. It's also a good solution if you want to die an early death.


text:
Beatles Interviews Database: Beatles Interview: AP & CBS News, Plaza Hotel 2/10/1964
ABOUT THIS INTERVIEW: On February 10th 1964, after breaking American television ratings records with their first appearance on the Ed Sullivan Show the night before, the Beatles again met with the American press.
In the following interviews, part of a day-long press conference consisting of one-on-one meetings with the press, both CBS News and the Associated Press asked the Beatles about a negative review of their Sullivan performance by a professional music critic. Once again, the Beatles showed they could charm not only the American public, but also the American press with their humor. These brief interviews were filmed by CBS-TV and the Associated Press in the Terrace Room of the Plaza Hotel in New York City.
As a bit of historical trivia, the Beatles are asked in passing about their opinions of the Keeler/Profumo affair. This was a recent sex scandal, involving a British model and a British government minister, which would disgrace the then-current conservative government in Britain.
The next day, the Beatles would travel to Washington D.C. for their February 11th performance at Washington Coliseum, their first American concert.
- Jay Spangler, www.beatlesinterviews.org
Associated Press interview:
Q: "Your program was reviewed by a music critic, and uhh..."
RINGO: (comically exaggerated) "OH NO!!!"
RINGO: "Not again!!!"
Q: (reading from newspaper) "...and he said that you had 'unresolved leading tones, a false modal frame ending up as a plain diatonic.' What would you say to that?"
JOHN: "He ought to see a doctor about that."
PAUL: "No, he's just copying the fella in the London Times who did a review like that. The only difference is, I don't know if that's favorable or not. The London one WAS, you see."
RINGO: "Why doesn't he just say if it's good or bad."
Q: (laughs) "What do you think of the reaction of Amer... What do you think of American girls in the first place, now that you've had your first contact with them?"
PAUL: "Marvelous. They're the same as British girls only they've got a different accent."
Q: "Is it your ambition to continue what you're doing, or what do you plan to do?"
JOHN: "Yeah."
PAUL: "Well, as long as it lasts like this. As long as we keep enjoying it. Once we stop enjoying it, and people stop enjoying it, then uhh..."
GEORGE: "We'll pack in."
RINGO: "We'll HAVE to pack in if people don't enjoy it."
GEORGE: "That's a British saying for 'give up.'"
Q: "One of the big attractions is your haircut and your manner of dress. What prompted that? The haircuts, for instance."
RINGO: "It just happened, you know. (looks up at his own hair) You just wake up one day and there you are."
CBS News interview:
Q: "Who came up with the name Beatles, and what does it really mean?"
RINGO: "John thought of the name Beatles, and he'll tell you about it NOW."
(Beatles laugh)
JOHN: "It means Beatles, doesn't it. But that's just a name, you know, like shoe."
PAUL: The Shoes, you see? We could have been called The Shoes for all you know."
Q: "There have been huge crowds of teenage girls outside complaining that they don't want to mob you, they just want to speak to you. What do you think about this? Do you want to talk to them?"
RINGO: "Well, have you ever tried talking to about two hundred people at once?"
JOHN: "We'd love to, you know. If we wave, somebody always says 'Stop that waving! You're inciting them!'"
Q: "How do you feel about your appearance at Carnegie Hall this week, the center of musical culture?"
RINGO: "Umm well, a bit nervous, but not too much, you know. We just hope we go down, 'cuz we're on with a lot of Americans. So we hope they like us."
Q: "What have you done in New York, and what do you hope to see while you're here?"
JOHN: "Well, we've been out to a couple of clubs. Rock clubs, mainly. 'Cuz we just like listening to rock when we're not working, anyway. And we're hoping some of our favorite artists will come up, but you know, they're all out on the road or something. They're not here."
Q: "Who is your favorite American artist?"
JOHN: "There's a lot, you know. Marvin Gaye, Miracles, Mary Wells. Those people."
Q: "When was the last time you had a haircut?"
PAUL: "Uhh, quite a long time, actually. (jokingly, but straightfaced) The last time was about 23 years ago, was the last time I had one."
Q: "Have you met any interesting American girls?"
GEORGE: "Umm... not yet, but hoping."
Q: "What do you think of the police protection you've been receiving here in this city?"
RINGO: "It's marvelous. They're doing a great job, you know, looking after us."
Q: "Are the crowds as large as you expected?"
JOHN: (giggling) "No. We didn't expect anything like this, you know."
Q: (aiming the microphone at Paul) "What is the most interesting offer you've ever received?"
PAUL: "Ahh... Yes... Umm... We've had a lot of interesting offers, actually. (pause) No comment!"
Q: (laughs) "What do you think of the Christine Keeler Profumo affair?"
GEORGE: (dryly) "It's great, yeah."
PAUL: (laughing) "Good publicity!"
GEORGE: (laughing) "Yeah."
JOHN: "They're all happening."
Q: "Have you heard any reviews of your appearance last night?"
PAUL: "The papers this morning just sort of... we only read two of them, and they weren't very favorable. I think one of the microphones was off, so that may..."
JOHN: "The kids still liked it, you know."
PAUL: "Yeah. The audience was fantastic. Great reception."
Q: "Thank you, gentlemen."
BEATLES: "Thank you!"
Source: Transcribed by www.beatlesinterviews.org from video copies of the interviews
Return to Database Menu


Sittin' Pretty in a Pretty Little City Yerkes Jazarimba Band Yerkes Dance Records Disc 192?
I Want A Boy ? Rex F 5137 - B Vertical record 192?
Pagliacci - Vesti la guibba Enrico Caruso Victor 88061 Disc 190?
Forza del Destino - Solenne Enrico Caruso and Antonio Scotti Victrola 89001 12" 190?
L'Elisir D'amore Allessandro Bonci Fonotopia 92100 Disc 191?
What Do You Do Sunday Mary? The Avalonians Bell P259-A Disc 192?
Last Night Was The End Of The World Henry Burr Cort F477 Disc 192?
Yerkes Dance Records was run by Harry A. Yerkes, and he apparently used equipment purchased from John Fletcher's Olympic Record Co. Most Yerkes discs are from about 1924.
Awesome show tonight Mac, enjoyed all the selections. Ed.
Sounds like a very early electric! If so, that would be 1925. Record companies switched over to microphones in 1925 from doing horn acoustic recordings.
Except for one exception was Edison! He was still doing acoustical up til late 1928! Finally switched over to electrical recordings in late 1928 and did some in October 1929 when the company went out of business!


text:
import { artefact, asset, tween, radian, constructors, styles, stylesnames, cell, cellnames, group, canvas } from '../core/library.js';

import { generateUniqueString, isa_canvas, mergeOver, λthis, λnull, Ωempty } from '../core/utilities.js';

import { scrawlCanvasHold } from '../core/document.js';

import { getPixelRatio, getIgnorePixelRatio } from "../core/events.js";

import { makeGroup } from './group.js';
import { makeState } from './state.js';
import { makeCoordinate, requestCoordinate, releaseCoordinate } from './coordinate.js';
import { filterEngine } from './filterEngine.js';
import { importDomImage } from './imageAsset.js';

import baseMix from '../mixin/base.js';
import positionMix from '../mixin/position.js';
import deltaMix from '../mixin/delta.js';
import pivotMix from '../mixin/pivot.js';
import mimicMix from '../mixin/mimic.js';
import pathMix from '../mixin/path.js';
import anchorMix from '../mixin/anchor.js';
import cascadeMix from '../mixin/cascade.js';
import assetMix from '../mixin/asset.js';
import patternMix from '../mixin/pattern.js';
import filterMix from '../mixin/filter.js';

// #### Cell constructor
const Cell = function (items = Ωempty) {

    this.makeName(items.name);

    if (!items.isPool) this.register();

    this.initializePositions();
    this.initializeCascade();

    if (!isa_canvas(items.element)) {

        let mycanvas = document.createElement('canvas');
        mycanvas.id = this.name;

        mycanvas.width = 300;
        mycanvas.height = 150;
        items.element = mycanvas;
    }

    this.installElement(items.element);

    if (items.isPool) this.set(this.poolDefs)
    else this.set(this.defs);

    this.set(items);

    this.state.setStateFromEngine(this.engine);

    if (!items.isPool) {

        makeGroup({
            name: this.name,
            host: this.name
        });
    }

    this.subscribers = [];
    this.sourceNaturalDimensions = makeCoordinate();
    this.dirtyDimensionsOverride = true;

    this.sourceLoaded = true;

    this.here = {};

    return this;
};

// #### Cell prototype
let P = Cell.prototype = Object.create(Object.prototype);
P.type = 'Cell';
P.lib = 'cell';
P.isArtefact = false;
P.isAsset = true;

// #### Mixins
// + [base](../mixin/base.html)
// + [asset](../mixin/asset.html) - goes above position mixin because asset objects use the kill function defined in this mixin, but Cells need to follow the position mixin formula (which includes a call to the factoryKill function)
// + [position](../mixin/position.html)
// + [delta](../mixin/delta.html)
// + [pivot](../mixin/pivot.html)
// + [mimic](../mixin/mimic.html)
// + [path](../mixin/path.html)
// + [anchor](../mixin/anchor.html)
// + [cascade](../mixin/cascade.html)
// + [pattern](../mixin/pattern.html)
// + [filter](../mixin/filter.html)
P = baseMix(P);
P = assetMix(P);
P = positionMix(P);
P = deltaMix(P);
P = pivotMix(P);
P = mimicMix(P);
P = pathMix(P);
P = anchorMix(P);
P = cascadeMix(P);
P = patternMix(P);
P = filterMix(P);

// #### Cell attributes
// + Attributes defined in the [base mixin](../mixin/base.html): __name__.
// + Attributes defined in the [position mixin](../mixin/position.html): __group, visibility, order, start, _startX_, _startY_, handle, _handleX_, _handleY_, offset, _offsetX_, _offsetY_, dimensions, _width_, _height_, pivoted, mimicked, lockTo, _lockXTo_, _lockYTo_, scale, roll, noUserInteraction, noPositionDependencies, noCanvasEngineUpdates, noFilters, noPathUpdates, purge, bringToFrontOnDrag__.
// + Attributes defined in the [delta mixin](../mixin/delta.html): __delta, noDeltaUpdates__.
// + Attributes defined in the [pivot mixin](../mixin/pivot.html): __pivot, pivotCorner, addPivotHandle, addPivotOffset, addPivotRotation__.
// + Attributes defined in the [mimic mixin](../mixin/mimic.html): __mimic, useMimicDimensions, useMimicScale, useMimicStart, useMimicHandle, useMimicOffset, useMimicRotation, useMimicFlip, addOwnDimensionsToMimic, addOwnScaleToMimic, addOwnStartToMimic, addOwnHandleToMimic, addOwnOffsetToMimic, addOwnRotationToMimic__.
// + Attributes defined in the [path mixin](../mixin/path.html): __path, pathPosition, addPathHandle, addPathOffset, addPathRotation, constantPathSpeed__.
// + Attributes defined in the [anchor mixin](../mixin/anchor.html): __anchor__.
// + Attributes defined in the [filter mixin](../mixin/filter.html): __filters, isStencil__.
// + Attributes defined in the [cascade mixin](../mixin/cascade.html): __groups__.
// + Attributes defined in the [pattern mixin](../mixin/pattern.html): __repeat, patternMatrix, matrixA, matrixB, matrixC, matrixD, matrixE, matrixF__.
// + Attributes defined in the [asset mixin](../mixin/asset.html): __source, subscribers__.
let defaultAttributes = {

// The following booleans determine whether a Cell canvas will, clear, compile and/or show itself as part of the Display cycle.
// + Note that as part of the Display cycle, Scrawl-canvas will complete all tasks of the `clear` part of the process before moving on to the `compile` stage, which again will complete before the `show` stage triggers.

// __cleared__ - Clearing the cell wipes it clean ready for new drawing activity - for cells that contain static imagery (such as a background) that only needs to be drawn once during an animation it makes no sense to construct its display on each iteration of the Display cycle. Can also be set to false if the compile step builds on, rather than replaces, the cell's current imagery.
    cleared: true,

// __compiled__ - Compiling the cell triggers the 'stamp cascade', where (visible) entitys in (visible) groups assigned to the cell are instructed to stamp themselves onto the cell. Again, set this to false if the cell's imagery does not need to be redrawn on each iteration of the Display cycle.
    compiled: true,

// __shown__ - Showing the cell instructs it to stamp itself onto the 'base' cell or, for base cells, to stamp itself onto the display <canvas> element in line with that canvas's wrapper's 'fit' attribute. This can be switched off if the cell is (for instance) being used as an asset source for a Picture entity or Pattern style.
    shown: true,

// Cells will compile and show in the order given (ascending) of their __compileOrder__ and __showOrder__ values. Cells sharing a compileOrder or showOrder value will be compiled and shown determined by the order in which they were declared in the script where they were created.
    compileOrder: 0,
    showOrder: 0,

// By default, cells have a background color of `rgba(0,0,0,0)` - transparent black, which gets applied as the end step in the clear part of the display cycle. Setting the __backgroundColor__ attribute ensures the Cell will use that color instead. Any CSS color String is a valid argument (but not gradients or patterns, which get applied at a later stage in the Display cycle).
// + Base cells can have this attribute set via their controller Canvas.
    backgroundColor: '',

// __clearAlpha__ - a Number with a value between 0 and 1. When not zero, the cell will not clear itself; rather it will copy its current contents, clear itself, set its globalAlpha to this value, copy back its contents (now faded) and then restore its globalAlpha value
    clearAlpha: 0,

// Non-base Cells will stamp themselves onto the 'base' Cell as part of the Display cycle's show stage. We can mediate this action by setting the Cell's __alpha__ and __composite__ attributes to valid Rendering2DContext `globalAlpha` and `globalCompositeOperation` values.
    alpha: 1,
    composite: 'source-over',

// We can also scale the Cell's size in the displayed Canvas by setting the __scale__ attribute to an appropriate value.
    scale: 1,

// __flipReverse__, __flipUpend__ - Boolean flags which determine the orientation of the cell when it stamps itself on the display.
// + a `reversed` cell is effectively flipped 180° around a vertical line passing through that cell's rotation-reflection (start) point - a face looking to the right will now look to the left
// + an `upended` cell is effectively flipped 180° around a horizontal line passing through that cell's rotation-reflection (start) point - a normal face will now appear upside-down
    flipReverse: false,
    flipUpend: false,

// __filter__ - the Canvas 2D engine supports the [filter attribute](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter) on an experimental basis, thus it is not guaranteed to work in all browsers and devices. The filter attribute takes a String value (default: 'none') defining one or more filter functions to be applied to the Cell as it is stamped on its host canvas.
// + Be aware that Cells can also take a `filters` Array - this represents an array of Scrawl-canvas filters to be applied to the Cell. The two filter systems are completely separate - combine their effects at your own risk!
    filter: 'none',

// Scrawl-canvas sets the following attributes automatically; do not change their values!

// __isBase__ - Every displayed <canvas> element - wrapped in a Scrawl-canvas Canvas object (factory/canvas.js) - must possess at least one Cell object, known as its 'base' Cell.
    isBase: false,

// __useAsPattern__ - Used to ignore the requirement to resize canvases to take into account device pixel ratios greater than 1
    useAsPattern: false,

// __controller__ - A reference link to the displayed <canvas> element's Scrawl-canvas wrapper (factory/canvas.js) - only 'base' cells require this handle.
    controller: null,

// __includeInCascadeEventActions__ - if a non-base Cell has its `shown` flag set to true, then it is automatically included in CascadeEventActions functionality. In situations where we don't want the Cell to _directly_ appear in the canvas, but do want to include it in CascadeEventActions, then we can set this flag to `true`
    includeInCascadeEventActions: false,
};
P.defs = mergeOver(P.defs, defaultAttributes);

// Cells don't have a need for these default attributes, which will have been added in by mixin/asset.js
delete P.defs.source;
delete P.defs.sourceLoaded;

// #### Packet/Clone management
// This functionality is disabled for Cell objects
P.stringifyFunction = λnull;
P.processPacketOut = λnull;
P.finalizePacketOut = λnull;
P.saveAsPacket = function () {

    return `[${this.name}, ${this.type}, ${this.lib}, {}]`
};
P.clone = λthis;

// #### Kill functionality
P.factoryKill = function () {

    let myname = this.name

    // Remove artefact from all canvases
    Object.entries(canvas).forEach(([name, cvs]) => {

        if (cvs.cells.indexOf(myname) >= 0) cvs.removeCell(myname);

        if (cvs.base && cvs.base.name === myname) {

            cvs.set({
                visibility: false,
            });
        }
    });

    // Remove from other artefacts
    Object.entries(artefact).forEach(([name, art]) => {

        if (art.name !== myname) {

            let state = art.state;

            if (state) {

                let fill = state.fillStyle,
                    stroke = state.strokeStyle;

                if (fill.name && fill.name === myname) state.fillStyle = state.defs.fillStyle;
                if (stroke.name && stroke.name === myname) state.strokeStyle = state.defs.strokeStyle;
            }
        }
    });

    // Kill group
    if (group[myname]) group[myname].kill();
};

// #### Get, Set, deltaSet
let G = P.getters,
    S = P.setters,
    D = P.deltaSetters;

// `get` - overwrites the mixin/position function
P.get = function (item) {

    let getter = this.getters[item];

    if (getter) return getter.call(this);

    else {

        let def = this.defs[item],
            state = this.state,
            val;

        if (typeof def != 'undefined') {

            val = this[item];
            return (typeof val != 'undefined') ? val : def;
        }

        def = state.defs[item];

        if (typeof def != 'undefined') {

            val = state[item];
            return (typeof val != 'undefined') ? val : def;
        }
        return undef;
    }
};

// `width`
G.width = function () {

    return this.currentDimensions[0] || this.element.getAttribute('width');
};
S.width = function (val) {

    if (val != null) {

        this.dimensions[0] = val;
        this.dirtyDimensions = true;
        this.dirtyDimensionsOverride = true;
    }
};

// `height`
G.height = function () {

    return this.currentDimensions[1] || this.element.getAttribute('height');
};
S.height = function (val) {

    if (val != null) {

        this.dimensions[1] = val;
        this.dirtyDimensions = true;
        this.dirtyDimensionsOverride = true;
    }
};

G.dimensions = function () {

    const w = this.currentDimensions[0] || this.element.getAttribute('width');
    const h = this.currentDimensions[1] || this.element.getAttribute('height');

    return [w, h];
};
S.dimensions = function (w, h) {

    this.setCoordinateHelper('dimensions', w, h);
    this.dirtyDimensions = true;
    this.dirtyDimensionsOverride = true;
};

// Internal setters
S.source = function () {};
S.engine = function (item) {};
S.state = function (item) {};

S.element = function (item) {

    if(isa_canvas(item)) this.installElement(item);
};

// Display cycle Boolean flags `cleared`, `compiled`, `shown`
S.backgroundColor = function (item) {

    // If we try to clear the cell with a transparent color, it will not clear. Setting it to an empty string fixes this issue
    if ('rgba(0, 0, 0, 0)' === item || 'transparent' === item || '#00000000' === item || '#0000' === item) item = '';
    this.backgroundColor = item;
};

S.cleared = function (item) {

    this.cleared = item;
    this.updateControllerCells();
};
S.compiled = function (item) {

    this.compiled = item;
    this.updateControllerCells();
};
S.shown = function (item) {

    this.shown = item;
    this.updateControllerCells();
};

// Display cycle order attributes `compileOrder`, `showOrder` - argument should be a positive integer Number
S.compileOrder = function (item) {

    this.compileOrder = item;
    this.updateControllerCells();
};
S.showOrder = function (item) {

    this.showOrder = item;
    this.updateControllerCells();
};

// __Stash coordinates and dimensions__ - which are stored in Coordinate arrays - allow us to store, and export to an image asset, a portion of the Cell's current display which can then be used (for instance) by Pattern styles.
// + Scrawl-canvas supplies the following _pseudo-attributes_ for dealing with the stash start coordinate (`stashX`, `stashY`) and dimensions (`stashWidth`, `stashHeight`)
// + Each of these values can be either absolute px Numbers, or relative (to the Cell's own dimensions) '%' Strings
S.stashX = function (val) {

    if (!this.stashCoordinates) this.stashCoordinates = [0, 0];
    this.stashCoordinates[0] = val;
};
S.stashY = function (val) {

    if (!this.stashCoordinates) this.stashCoordinates = [0, 0];
    this.stashCoordinates[1] = val;
};
S.stashWidth = function (val) {

    if (!this.stashDimensions) {

        let dims = this.currentDimensions;
        this.stashDimensions = [dims[0], dims[1]];
    }
    this.stashDimensions[0] = val;
};
S.stashHeight = function (val) {

    if (!this.stashDimensions) {

        let dims = this.currentDimensions;
        this.stashDimensions = [dims[0], dims[1]];
    }
    this.stashDimensions[1] = val;
};
D.stashX = function (val) {

    if (!this.stashCoordinates) this.stashCoordinates = [0, 0];

    let c = this.stashCoordinates;
    c[0] = addStrings(c[0], val);
};
D.stashY = function (val) {

    if (!this.stashCoordinates) this.stashCoordinates = [0, 0];

    let c = this.stashCoordinates;
    c[1] = addStrings(c[1], val);
};
D.stashWidth = function (val) {

    if (!this.stashDimensions) {

        let dims = this.currentDimensions;
        this.stashDimensions = [dims[0], dims[1]];
    }

    let c = this.stashDimensions;
    c[0] = addStrings(c[0], val);
};
D.stashHeight = function (val) {

    if (!this.stashDimensions) {

        let dims = this.currentDimensions;
        this.stashDimensions = [dims[0], dims[1]];
    }

    let c = this.stashDimensions;
    c[1] = addStrings(c[1], val);
};

S.clearAlpha = function (val) {

    if (val.toFixed) {

        if (val > 1) val = 1;
        else if (val < 0) val = 0;

        this.clearAlpha = val;
    }
};
D.clearAlpha = function (val) {

    if (val.toFixed) {

        val += this.clearAlpha;

        if (val > 1) val = 1;
        else if (val < 0) val = 0;

        this.clearAlpha = val;
    }
};

// `smoothFont` - handle this directly; don't save the attribute state
S.smoothFont = function (item) {

    const { element } = this;

    if (element) {

        const { style } = element;

        if (style) {

            if (item) {
                style['webkitFontSmoothing'] = 'auto';
                style['mozOsxFontSmoothing'] = 'auto';
                style['smoothFont'] = 'auto';
            }
            else {
                style['webkitFontSmoothing'] = 'none';
                style['mozOsxFontSmoothing'] = 'grayscale';
                style['smoothFont'] = 'never';
            }
        }
    }
};

// `checkForEntityHover`, `onEntityHover`, `onEntityNoHover` - these are group-specific attributes which we can set on the Cell's named group via the Cell's wrapper
S.checkForEntityHover = function (item) {

    group[this.name].set({
        checkForEntityHover: item,
    });
};
S.onEntityHover = function (item) {

    group[this.name].set({
        onEntityHover: item,
    });
};
S.onEntityNoHover = function (item) {

    group[this.name].set({
        onEntityNoHover: item,
    });
};

// #### Prototype functions

// `checkSource` - internal function
P.checkSource = function (width, height) {

    if (this.currentDimensions[0] !== width || this.currentDimensions[1] !== height) this.notifySubscribers();
};

// `getData` - internal function, invoked when a Cell wrapper is used as an entity's pattern style
P.getData = function (entity, cell) {

    this.checkSource(this.sourceNaturalDimensions[0], this.sourceNaturalDimensions[1]);

    return this.buildStyle(cell);
};

// `updateArtefacts` - passes the __items__ argument object through to each of the Cell's Groups for forwarding to their artefacts' `setDelta` function
P.updateArtefacts = function (items = Ωempty) {

    const gb = this.groupBuckets;

    let art, ab, i, iz, j, jz;

    for (i = 0, iz = gb.length; i < iz; i++) {

        ab = gb[i].artefactCalculateBuckets;

        for (j = 0, jz = ab.length; j < jz; j++) {

            art = ab[j];

            if (items.dirtyScale) art.dirtyScale = true;
            if (items.dirtyDimensions) art.dirtyDimensions = true;
            if (items.dirtyLock) art.dirtyLock = true;
            if (items.dirtyStart) art.dirtyStart = true;
            if (items.dirtyOffset) art.dirtyOffset = true;
            if (items.dirtyHandle) art.dirtyHandle = true;
            if (items.dirtyRotation) art.dirtyRotation = true;
            if (items.dirtyPathObject) art.dirtyPathObject = true;
        }
    }
};

// `cleanDimensionsAdditionalActions` - overwrites mixin/position function:
// + Updates the Cell's <canvas> element's dimensions
// + Restores the render engine's attributes to current cell values (because the resize wipes them to default values)
// + Asks the Canvas controller to trigger an update to the Cell's `here` object
// + Tells all associated artefacts that the Cell's dimensions have changed
P.cleanDimensionsAdditionalActions = function() {

    let element = this.element;

    if (element) {

        let control = this.controller,
            current = this.currentDimensions,
            base = this.isBase;

        let ignoreDpr = getIgnorePixelRatio();
        let dpr = getPixelRatio();

        // DEPRECATED (because it is a really bad name) __isComponent__ replaced by __baseMatchesCanvasDimensions__
        if (this.cleared || this.dirtyDimensionsOverride) {

            this.dirtyDimensionsOverride = false;

            if (ignoreDpr) {

                if (base && control && (control.baseMatchesCanvasDimensions || control.isComponent)) {

                    let controlDims = this.controller.currentDimensions,
                        dims = this.dimensions;

                    dims[0] = current[0] = controlDims[0];
                    dims[1] = current[1] = controlDims[1];
                }

                let [w, h] = current;

                element.width = w;
                element.height = h;
            }
            else {

                if (base && control && (control.baseMatchesCanvasDimensions || control.isComponent)) {

                    let controlDims = this.controller.currentDimensions,
                        dims = this.dimensions;

                    dims[0] = current[0] = controlDims[0];
                    dims[1] = current[1] = controlDims[1];
                }

                let [w, h] = current;

                if (ignoreDpr) {

                    element.width = w;
                    element.height = h;
                }
                else {

                    element.width = w * dpr;
                    element.height = h * dpr;
                }
            }

            this.setEngineFromState(this.engine);

            if (base && control) control.updateBaseHere();

            if (this.groupBuckets) {

                this.updateArtefacts({
                    dirtyDimensions: true,
                });
            }
        }
    }
};

// `notifySubscriber` - Overrides mixin/asset.js function
P.notifySubscriber = function (sub) {

    if (!sub.sourceNaturalDimensions) sub.sourceNaturalDimensions = [];

    sub.sourceNaturalWidth = this.currentDimensions[0];
    sub.sourceNaturalHeight = this.currentDimensions[1];

    sub.sourceLoaded = true;
    sub.dirtyImage = true;

    sub.dirtyCopyStart = true;
    sub.dirtyCopyDimensions = true;
};

// `subscribeAction` - Overrides mixin/asset.js function
P.subscribeAction = function (sub = {}) {

    this.subscribers.push(sub);
    sub.asset = this;
    sub.source = this.element;
    this.notifySubscriber(sub)
};

// `installElement` - internal function, used by the constructor
P.installElement = function (element) {

    this.element = element;
    this.engine = this.element.getContext('2d');

    this.state = makeState({
        engine: this.engine,
    });

    return this;
};

// `updateControllerCells` - internal function: ask the Cell's Canvas controller to review/update its cells data
P.updateControllerCells = function () {

    const { controller, currentHost } = this;

    if (controller) controller.dirtyCells = true;
    else if (currentHost) {

        let host = currentHost.getHost();

        if (host) host.dirtyCells = true;
    }
};

// The following functions set the Cell wrapper's <canvas> element's CanvasRenderingContext2D engine's attributes to match a given set of values

// `setEngineFromState` - internal function: set engine to match this Cell's State object's attribute values
P.setEngineFromState = function (engine) {

    const state = this.state,
        stateKeys = state.allKeys,
        stateKeysLen = stateKeys.length;

    let i, iz, key, eVal, sVal;

    for (i = 0; i < stateKeysLen; i++) {

        key = stateKeys[i];
        eVal = engine[key];
        sVal = state[key];

        if (key === 'lineDash') {

            engine.lineDash = sVal;
            engine.setLineDash(engine.lineDash);
        }
        else if (eVal !== sVal) engine[key] = sVal;
    }
    if (engine.textAlign !== 'left') engine.textAlign = 'left';
    if (engine.textBaseline !== 'top') engine.textBaseline = 'top';

    return this;
};

// `setToDefaults` - internal function: set engine to match the State Factory's default attribute values
P.setToDefaults = function () {

    let items = this.state.defs,
        state = this.state,
        engine = this.engine,
        isArray = Array.isArray;

    Object.entries(items).forEach(([key, value]) => {

        if (key === 'lineDash') {

            if (!isArray(engine.lineDash)) engine.lineDash = [];
            else engine.lineDash.length = 0;

            if (!isArray(state.lineDash)) state.lineDash = [];
            else state.lineDash.length = 0;
        }
        else {

            engine[key] = value;
            state[key] = value;
        }
    });

    engine.textAlign = state.textAlign = 'left';
    engine.textBaseline = state.textBaseline = 'top';

    return this;
};

// `setEngine` - internal function: set engine to match the entity object's State attribute values
P.stylesArray = ['Gradient', 'RadialGradient', 'Pattern'];
P.setEngine = function (entity) {

    const state = this.state,
        entityState = entity.state;

    if (entityState) {

        let engine, item,
            changes = entityState.getChanges(entity, state),
            action = this.setEngineActions,
            stylesArray = this.stylesArray;

        if (Object.keys(changes).length) {

            engine = this.engine;

            for (item in changes) {

                action[item](changes[item], engine, stylesArray, entity, this);
                state[item] = changes[item];
            }
        }
    }
    return entity;
};

// __setEngineActions__ - an Object containing functions for updating the engine's attributes; used by `setEngine`
P.setEngineActions = {

    fillStyle: function (item, engine, stylesArray, entity, layer) {

        if (item.substring) {

            let brokenStyle = false;

            if (stylesnames.indexOf(item) >= 0) brokenStyle = styles[item];
            else if (cellnames.indexOf(item) >= 0) brokenStyle = cell[item];

            if (brokenStyle) {

                entity.state.fillStyle = brokenStyle;
                engine.fillStyle = brokenStyle.getData(entity, layer);
            }
            else engine.fillStyle = item;
        }
        else engine.fillStyle = item.getData(entity, layer);
    },

    filter: function (item, engine) {
        engine.filter = item;
    },

    font: function (item, engine) {
        engine.font = item;
    },

    globalAlpha: function (item, engine) {
        engine.globalAlpha = item;
    },

    globalCompositeOperation: function (item, engine) {
        engine.globalCompositeOperation = item;
    },

    imageSmoothingEnabled: function (item, engine) {
        engine.imageSmoothingEnabled = item;
    },

    imageSmoothingQuality: function (item, engine) {
        engine.imageSmoothingQuality = item;
    },

    lineCap: function (item, engine) {
        engine.lineCap = item;
    },

    lineDash: function (item, engine) {
        engine.lineDash = item;
        if (engine.setLineDash) engine.setLineDash(item);
    },

    lineDashOffset: function (item, engine) {
        engine.lineDashOffset = item;
    },

    lineJoin: function (item, engine) {
        engine.lineJoin = item;
    },

    lineWidth: function (item, engine) {
        engine.lineWidth = item;
    },

    miterLimit: function (item, engine) {
        engine.miterLimit = item;
    },

    shadowBlur: function (item, engine) {
        engine.shadowBlur = item;
    },

    shadowColor: function (item, engine) {
        engine.shadowColor = item;
    },

    shadowOffsetX: function (item, engine) {
        engine.shadowOffsetX = item;
    },

    shadowOffsetY: function (item, engine) {
        engine.shadowOffsetY = item;
    },

    strokeStyle: function (item, engine, stylesArray, entity, layer) {

        if (item.substring) {

            let brokenStyle = false;

            if (stylesnames.indexOf(item) >= 0) brokenStyle = styles[item];
            else if (cellnames.indexOf(item) >= 0) brokenStyle = cell[item];

            if (brokenStyle) {

                entity.state.strokeStyle = brokenStyle;
                engine.strokeStyle = brokenStyle.getData(entity, layer);
            }
            else engine.strokeStyle = item;
        }
        else engine.strokeStyle = item.getData(entity, layer);
    },
};

// The following functions are used as part of entity object `stamp` functionality - specifically for those with a __method__ whose appearance is affected by shadows, and for the `clear` method
// + Scrawl-canvas, for the most part, avoids using engine.save() and engine.restore() functionality, instead preferring to keep track of engine state in State objects.
// + When clearing and restoring shadows - a frequent operation given Scrawl-canvas functionality around stamping methods - both the Cell's state object and the Canvas context engine need to be updated with the necessary data
// + The same reasoning holds when setting up the context engine to 'clear' an entity from the canvas display instead of stamping it onto the canvas

// `clearShadow`
P.clearShadow = function () {

    this.engine.shadowOffsetX = 0.0;
    this.engine.shadowOffsetY = 0.0;
    this.engine.shadowBlur = 0.0;
    this.state.shadowOffsetX = 0.0;
    this.state.shadowOffsetY = 0.0;
    this.state.shadowBlur = 0.0;

    return this;
};

// `restoreShadow`
P.restoreShadow = function (entity) {

    let state = entity.state;

    this.engine.shadowOffsetX = state.shadowOffsetX;
    this.engine.shadowOffsetY = state.shadowOffsetY;
    this.engine.shadowBlur = state.shadowBlur;
    this.state.shadowOffsetX = state.shadowOffsetX;
    this.state.shadowOffsetY = state.shadowOffsetY;
    this.state.shadowBlur = state.shadowBlur;

    return this;
};

// `setToClearShape`
P.setToClearShape = function () {

    this.engine.fillStyle = 'rgba(0,0,0,0)';
    this.engine.strokeStyle = 'rgba(0,0,0,0)';
    this.engine.shadowColor = 'rgba(0,0,0,0)';
    this.state.fillStyle = 'rgba(0,0,0,0)';
    this.state.strokeStyle = 'rgba(0,0,0,0)';
    this.state.shadowColor = 'rgba(0,0,0,0)';

    return this;
};

// `saveEngine`, `restoreEngine` - save and restore the Cell wrapper's <canvas> element's CanvasRenderingContext2D engine's current state to/from a stack
P.saveEngine = function () {

    this.engine.save();
    return this;
};

P.restoreEngine = function () {

    this.engine.restore();
    return this;
};

// `getComputedFontSizes` - internal function - the Cell wrapper gets passed by Phrase entitys to its fontAttributes object, which then invokes it when calculating font sizes
P.getComputedFontSizes = function () {

    let host = this.getHost();

    if (host && host.domElement) {

        let em = window.getComputedStyle(host.domElement),
            rem = window.getComputedStyle(document.documentElement);

        return [parseFloat(em.fontSize), parseFloat(rem.fontSize), window.innerWidth, window.innerHeight];
    }
    return false;
}

// #### Display cycle functionality
// This functionality is triggered by the Cell's Canvas wrapper controller

// `checkEngineScale`
// DPR is detected in the `core/events.js` file, but mainly handled here
// + We scale the cell by DPR - this should be the only time we touch native scale functionality!
// + All the other scaling functionality in SC is handled by computiation - applying the scaling factor to dimensions, start, handle, offset etc values which then get saved in the `current` equivalent attributes
const checkEngineScale = function (engine) {

    if (engine) {

        engine.setTransform(1,0,0,1,0,0);

        if (getIgnorePixelRatio()) engine.scale(1, 1);
        else {

            const dpr = getPixelRatio();
            engine.scale(dpr, dpr);
            return dpr;
        }
    }
    return 1;
};

// `clear`
P.clear = function () {

    const {element, engine, backgroundColor, clearAlpha, currentDimensions} = this;
    const [width, height] = currentDimensions;

    this.prepareStamp();

    let dpr = checkEngineScale(engine);

    let w = width * dpr,
        h = height * dpr;

    if (this.useAsPattern) {

        element.width = width;
        element.height = height;
    }

    if (backgroundColor) {

        engine.save();
        engine.fillStyle = backgroundColor;
        engine.globalCompositeOperation = 'source-over';
        engine.globalAlpha = 1;
        engine.fillRect(0, 0, width, height);
        engine.restore();
    }
    else if (clearAlpha) {

        engine.save();
        let tempCell = requestCell();

        let {engine:tempEngine, element:tempEl} = tempCell;

        if (this.useAsPattern) {

            tempEl.width = width;
            tempEl.height = height;

            tempEngine.drawImage(element, 0, 0, width, height, 0, 0, width, height);

            engine.clearRect(0, 0, width, height);
            engine.globalAlpha = clearAlpha;

            engine.drawImage(tempEl, 0, 0, width, height, 0, 0, width, height);
        }
        else {
            tempEl.width = w;
            tempEl.height = h;

            tempEngine.drawImage(element, 0, 0, width, height, 0, 0, width, height);

            engine.clearRect(0, 0, width, height);
            engine.globalAlpha = clearAlpha;

            engine.drawImage(tempEl, 0, 0, w, h, 0, 0, width, height);
        }
        engine.restore();

        releaseCell(tempCell);
    }
    else engine.clearRect(0, 0, width, height);
};

// `compile`
P.compile = function(){

    let mystash = this.stashOutput;

    this.sortGroups();

    if (!this.cleared) this.prepareStamp();

    if(this.dirtyFilters || !this.currentFilters) this.cleanFilters();

    checkEngineScale(this.engine);

    const gb = this.groupBuckets,
        gbLen = gb.length;

    for (let i = 0, grp; i < gbLen; i++) {

        grp = gb[i];
        if (grp && grp.stamp) grp.stamp();
    }

    if (!this.noFilters && this.filters && this.filters.length) this.applyFilters();
    this.stashOutputAction();
};

// `show` - Note that functionality here differs for __base cells__ and other Cell wrappers
P.show = function () {

    // get the destination cell's canvas context
    let host = this.getHost(),
        engine = (host && host.engine) ? host.engine : false;

    if (engine) {

        let floor = Math.floor,
            hostDimensions = host.currentDimensions,
            destWidth = floor(hostDimensions[0]),
            destHeight = floor(hostDimensions[1]);

        // Cannot draw to the destination canvas if either of its dimensions === 0
        if (!destWidth || !destHeight) return false;

        let { currentScale:scale, currentDimensions, composite, alpha, controller, element, isBase, currentStampHandlePosition:handle, currentStampPosition:stamp } = this;

        let curWidth = floor(currentDimensions[0]),
            curHeight = floor(currentDimensions[1]),
            paste;

        engine.save();

        checkEngineScale(engine);

        engine.filter = this.filter;

        if (isBase) {

            if (!this.basePaste) this.basePaste = [];
            paste = this.basePaste;

            // copy the base canvas over to the display canvas. This copy operation ignores any scale, roll or position attributes set on the base cell, instead complying with the controller's fit attribute requirements
            if (!this.cleared && !this.compiled) this.prepareStamp();

            engine.globalCompositeOperation = 'source-over';
            engine.globalAlpha = 1;
            engine.clearRect(0, 0, destWidth, destHeight);

            engine.globalCompositeOperation = composite;
            engine.globalAlpha = alpha;

            let fit = (controller) ? controller.fit : 'none',
                relWidth, relHeight;

            switch (fit) {

                case 'contain' :
                    // base must copy into display resized, centered, letterboxing if necessary, maintaining aspect ratio
                    relWidth = destWidth / (curWidth || 1);
                    relHeight = destHeight / (curHeight || 1);

                    if (relWidth > relHeight) {

                        paste[0] = floor((destWidth - (curWidth * relHeight)) / 2);
                        paste[1] = 0;
                        paste[2] = floor(curWidth * relHeight);
                        paste[3] = floor(curHeight * relHeight);
                    }
                    else {

                        paste[0] = 0;
                        paste[1] = floor((destHeight - (curHeight * relWidth)) / 2);
                        paste[2] = floor(curWidth * relWidth);
                        paste[3] = floor(curHeight * relWidth);
                    }
                    break;

                case 'cover' :
                    // base must copy into display resized, centered, leaving no letterbox area, maintaining aspect ratio
                    relWidth = destWidth / (curWidth || 1);
                    relHeight = destHeight / (curHeight || 1);

                    if (relWidth < relHeight) {

                        paste[0] = floor((destWidth - (curWidth * relHeight)) / 2);
                        paste[1] = 0;
                        paste[2] = floor(curWidth * relHeight);
                        paste[3] = floor(curHeight * relHeight);
                    }
                    else{

                        paste[0] = 0;
                        paste[1] = floor((destHeight - (curHeight * relWidth)) / 2);
                        paste[2] = floor(curWidth * relWidth);
                        paste[3] = floor(curHeight * relWidth);
                    }
                    break;

                case 'fill' :
                    // base must copy into display resized, distorting the aspect ratio as necessary
                    paste[0] = 0;
                    paste[1] = 0;
                    paste[2] = floor(destWidth);
                    paste[3] = floor(destHeight);
                    break;

                case 'none' :
                default :
                    // base copies into display as-is, centred, maintaining aspect ratio
                    paste[0] = floor((destWidth - curWidth) / 2);
                    paste[1] = floor((destHeight - curHeight) / 2);
                    paste[2] = curWidth;
                    paste[3] = curHeight;
            }
        }
        else if (scale > 0) {

            if (!this.paste) this.paste = [];
            paste = this.paste;

            // Cell canvases are treated like entitys on the base canvas: they can be positioned, scaled and rotated. Positioning will respect lockTo; flipReverse and flipUpend; and can be pivoted to other artefacts, or follow a path entity, etc. If pivoted to the mouse, they will use the base canvas's .here attribute, which takes into account differences between the base and display canvas dimensions.

            if (!this.noDeltaUpdates) this.setDelta(this.delta);

            if (!this.cleared && !this.compiled) this.prepareStamp();

            engine.globalCompositeOperation = composite;
            engine.globalAlpha = alpha;

            paste[0] = floor(-handle[0] * scale);
            paste[1] = floor(-handle[1] * scale);
            paste[2] = floor(curWidth * scale);
            paste[3] = floor(curHeight * scale);

            this.rotateDestination(engine, ...stamp);
        }
        engine.drawImage(element, 0, 0, curWidth, curHeight, ...paste);
        engine.restore();
    }
};

// `applyFilters` - Internal function - add filters to the Cell's current output.
P.applyFilters = function () {

    let engine = this.engine;

    let image = engine.getImageData(0, 0, this.currentDimensions[0], this.currentDimensions[1]);

    this.preprocessFilters(this.currentFilters);

    let img = filterEngine.action({
        identifier: this.filterIdentifier,
        image: image,
        filters: this.currentFilters
    });

    if (img) engine.putImageData(img, 0, 0);
};

// `stashOutputAction` - Internal function - stash the Cell's current output. While this function can be called at any time, the simplest way to invoke it is to set the Cell's __stashOutput__ flag to true, which will then invoke this function at the end of the compile step of the Display cycle (after any filters have been applied to the cell display).
// + The simplest way to set the stashOutput flag is to call `scrawl.createImageFromCell(cellName_or_cellObject, stashOutputAsAsset_flag)` from the user code.
// + We can limit the area of the cell display to be stashed by setting the Cell's __stashX__, __stashY__, __stashWidth__ and __stashHeight__ values appropriately. These can all be either absolute (positive) number values, or %String number values relative to the Cell element's dimensions.
// + We store the generated imageData object into the Cell object's __stashedImageData__ attribute.
// + If we are also stashing an image, an <img> element will be generated and stored in the Cell object's __stashedImage__ attribute. We also generate an imageAsset wrapper for the object that will have the name `cellname+'-image'`, which gets added to the assets section of the Scrawl-canvas library.
P.stashOutputAction = function () {

    if (this.stashOutput) {

        this.stashOutput = false;

        let { currentDimensions, stashCoordinates, stashDimensions, engine } = this;

        let [cellWidth, cellHeight] = currentDimensions;

        let stashX = (stashCoordinates) ? stashCoordinates[0] : 0,
            stashY = (stashCoordinates) ? stashCoordinates[1] : 0,
            stashWidth = (stashDimensions) ? stashDimensions[0] : cellWidth,
            stashHeight = (stashDimensions) ? stashDimensions[1] : cellHeight;

        // Keep the stashed image within bounds of the Cell's dimensions.
        if (stashWidth.substring || stashHeight.substring || stashX.substring || stashY.substring || stashX || stashY || stashWidth !== cellWidth || stashHeight !== cellHeight) {

            if (stashWidth.substring) stashWidth = (parseFloat(stashWidth) / 100) * cellWidth;
            if (isNaN(stashWidth) || stashWidth <= 0) stashWidth = 1;
            if (stashWidth > cellWidth) stashWidth = cellWidth;

            if (stashHeight.substring) stashHeight = (parseFloat(stashHeight) / 100) * cellHeight;
            if (isNaN(stashHeight) || stashHeight <= 0) stashHeight = 1;
            if (stashHeight > cellHeight) stashHeight = cellHeight;

            if (stashX.substring) stashX = (parseFloat(stashX) / 100) * cellWidth;
            if (isNaN(stashX) || stashX < 0) stashX = 0;
            if (stashX + stashWidth > cellWidth) stashX = cellWidth - stashWidth;

            if (stashY.substring) stashY = (parseFloat(stashY) / 100) * cellHeight;
            if (isNaN(stashY) || stashY < 0) stashY = 0;
            if (stashY + stashHeight > cellHeight) stashY = cellHeight - stashHeight;
        }

        // Get the imageData object, and stash it
        engine.save();
        engine.setTransform(1, 0, 0, 1, 0, 0);
        this.stashedImageData = engine.getImageData(stashX, stashY, stashWidth, stashHeight);
        engine.restore();

        // Get the dataUrl String, updating the stashed <img> element with it
        if (this.stashOutputAsAsset) {

            this.stashOutputAsAsset = false;

            let sourcecanvas, mycanvas;

            mycanvas = requestCell();
            sourcecanvas = mycanvas.element;

            sourcecanvas.width = stashWidth;
            sourcecanvas.height = stashHeight;

            mycanvas.engine.putImageData(this.stashedImageData, 0, 0);

            if (!this.stashedImage) {

                let newimg = this.stashedImage = document.createElement('img');

                newimg.id = `${this.name}-image`;

                newimg.onload = function () {

                    scrawlCanvasHold.appendChild(newimg);
                    importDomImage(`#${newimg.id}`);
                };

                newimg.src = sourcecanvas.toDataURL();
            }
            else this.stashedImage.src = sourcecanvas.toDataURL();

            releaseCell(mycanvas);
        }
    }
};

// `getHost` - Internal function - get a reference to the Cell's current host (where it will be stamping itself as part of the Display cycle).
// + Note that Cells can (in theory: not tested yet) belong to more than one Canvas object Group - they can be used in multiple <canvas> elements, thus the need to check which canvas is the current host at this point in the Display cycle.
P.getHost = function () {

    if (this.currentHost) return this.currentHost;
    else if (this.host) {

        let host = asset[this.host] || artefact[this.host];

        if (host) this.currentHost = host;

        return (host) ? this.currentHost : false;
    }
    return false;
};

// `updateBaseHere` - Internal function - keeping the Canvas object's 'base' Cell's `.here` attribute up-to-date with accurate mouse/pointer/touch cursor data
P.updateBaseHere = function (controllerHere, fit) {

    if (this.isBase) {

        if (!this.here) this.here = {};
        let here = this.here,
            dims = this.currentDimensions;

        let active = controllerHere.active;

        let controllerWidth = (controllerHere.localListener) ? controllerHere.originalWidth : controllerHere.w;
        let controllerHeight = (controllerHere.localListener) ? controllerHere.originalHeight : controllerHere.h;

        if (dims[0] !== controllerWidth || dims[1] !== controllerHeight) {

            if (!this.basePaste) this.basePaste = [];

            let pasteX = this.basePaste[0];

            let localWidth = dims[0],
                localHeight = dims[1],
                remoteWidth = controllerWidth,
                remoteHeight = controllerHeight,
                remoteX = controllerHere.x,
                remoteY = controllerHere.y;

            let relWidth = localWidth / remoteWidth || 1,
                relHeight = localHeight / remoteHeight || 1,
                round = Math.round,
                offsetX, offsetY;

            here.w = localWidth;
            here.h = localHeight;

            switch (fit) {

                case 'contain' :
                case 'cover' :

                    if (pasteX) {

                        offsetX = (remoteWidth - (localWidth / relHeight)) / 2;

                        here.x = round((remoteX - offsetX) * relHeight);
                        here.y = round(remoteY * relHeight);
                    }
                    else {

                        offsetY = (remoteHeight - (localHeight / relWidth)) / 2;

                        here.x = round(remoteX * relWidth);
                        here.y = round((remoteY - offsetY) * relWidth);
                    }
                    break;

                case 'fill' :
                    here.x = round(remoteX * relWidth);
                    here.y = round(remoteY * relHeight);
                    break;

                case 'none' :
                default :
                    offsetX = (remoteWidth - localWidth) / 2;
                    offsetY = (remoteHeight - localHeight) / 2;

                    here.x = round(remoteX - offsetX);
                    here.y = round(remoteY - offsetY);
            }

            if (here.x < 0 || here.x > localWidth) active = false;
            if (here.y < 0 || here.y > localHeight) active = false;

            here.active = active;
        }
        else {

            here.x = controllerHere.x;
            here.y = controllerHere.y;
            here.w = controllerWidth;
            here.h = controllerHeight;
            here.active = active;
        }
        controllerHere.baseActive = active;
    }
};

// `prepareStamp` - Internal function - steps to be performed before the Cell stamps its visual contents onto a Canvas object's base cell's canvas. Will be invoked as part of the Display cycle 'show' functionality.
// + Cells can emulate (much of) the functionality of entity artefacts, in that they can be positioned (start, handle, offset), rotated, scaled and flipped when they stamp themselves on the base cell. They can also be positioned using mimic, pivot, path and mouse functionality.
// + Cells cannot be included in Group objects (which only accept artefacts as members); however drag-and-drop functionality can be emulated by creating a Block and pivot/mimic the Cell to that. Similarly, Block substitutes can be used for hover detection, with their hook functions tailored to pass on the required response to the Cell - see Demo [Canvas-036](../../demo/canvas-036.html).
// + Note that Cells acting as a Canvas object's 'base' cell will position themselves on the displayed Canvas in line with their Canvas controller's 'fit' attribute, disregarding any positional information it may have been given.
P.prepareStamp = function () {

    if (this.dirtyScale || this.dirtyDimensions || this.dirtyStart || this.dirtyOffset || this.dirtyHandle) this.dirtyPathObject = true;

    if (this.dirtyScale) this.cleanScale();

    if (this.dirtyDimensions) {

        this.cleanDimensions();
        this.dirtyAssetSubscribers = true;
    }

    if (this.dirtyLock) this.cleanLock();
    if (this.dirtyStart) this.cleanStart();
    if (this.dirtyOffset) this.cleanOffset();
    if (this.dirtyHandle) this.cleanHandle();
    if (this.dirtyRotation) this.cleanRotation();

    if (this.isBeingDragged || this.lockTo.indexOf('mouse') >= 0) {

        this.dirtyStampPositions = true;
        this.dirtyStampHandlePositions = true;
    }

    if (this.dirtyStampPositions) this.cleanStampPositions();
    if (this.dirtyStampHandlePositions) this.cleanStampHandlePositions();

    if (this.dirtyPathObject) this.cleanPathObject();

    if (this.dirtyPositionSubscribers) this.updatePositionSubscribers();

    if (this.dirtyAssetSubscribers) {

        this.dirtyAssetSubscribers = false;
        this.notifySubscribers();
    }
};

// `cleanPathObject` - Calculate the Cell's __Path2D object__
P.cleanPathObject = function () {

    this.dirtyPathObject = false;

    if (!this.noPathUpdates || !this.pathObject) {

        let p = this.pathObject = new Path2D();

        let handle = this.currentStampHandlePosition,
            scale = this.currentScale,
            dims = this.currentDimensions;

        let x = -handle[0] * scale,
            y = -handle[1] * scale,
            w = dims[0] * scale,
            h = dims[1] * scale;

        p.rect(x, y, w, h);
    }
};

// `updateHere` - Internal function - get the Cell to update its .here information
P.updateHere = function () {

    if (!this.here) this.here = {};

    let localHere = this.here;

    let [width, height] = this.currentDimensions;

    localHere.w = width;
    localHere.h = height;
    localHere.x = -10000;
    localHere.y = -10000;
    localHere.active = false;

    let host = this.currentHost;

    if (host) {

        let hostHere = host.here;

        if (hostHere && hostHere.active) {

            let {x:hostX, y:hostY, w:hostWidth, h:hostHeight} = hostHere;

            if (!this.pathObject || this.dirtyPathObject) this.cleanPathObject();

            let tempCell = requestCell();
            let tempEngine = tempCell.engine;

            let [stampX, stampY] = this.currentStampPosition;

            tempCell.rotateDestination(tempEngine, stampX, stampY, this);

            let active = tempEngine.isPointInPath(this.pathObject, hostX, hostY);

            releaseCell(tempCell);

            localHere.active = active;

            if (active) {

                let [stampHandleX, stampHandleY] = this.currentStampHandlePosition;

                let {flipUpend, flipReverse, roll, scale} = this;

                if (scale) {

                    let left = ((hostX - stampX) / scale),
                        top = ((hostY - stampY) / scale);

                    if (flipReverse) left = -left;
                    if (flipUpend) top = -top;

                    if (roll) {

                        if ((flipReverse && !flipUpend) || (!flipReverse && flipUpend)) roll = -roll;

                        let coord = requestCoordinate(left, top);
                        coord.rotate(-roll);

                        [left, top] = coord;

                        releaseCoordinate(coord);
                    }

                    left += stampHandleX;
                    top += stampHandleY;

                    localHere.x = left;
                    localHere.y = top;
                }
            }
        }
    }
};

// `getEntityHits` - Returns an array of entity Objects responding 'true' to a checkHit call on them, for the Cell's current `.here` attribute coordinates. Used in particular with `Canvas.cascadeEventAction()` function
P.getEntityHits = function () {

    let response = [],
        results = [],
        resultNames = [];

    if (this.groupBuckets) {

        this.groupBuckets.forEach(grp => {
            if (grp.visibility) results.push(grp.getAllArtefactsAt(this.here));
        }, this);
    }

    if (results.length) {

        results = results.reduce((a, v) => a.concat(v), []);

        results.forEach(item => {

            let art = item.artefact;

            if (art.visibility && resultNames.indexOf(art.name) < 0) {

                resultNames.push(art.name);
                response.push(art);
            }
        })
    }
    return response;
};

// `rotateDestination` - internal function, called by entity objects about to stamp themselves onto the Cell.
// + entity stamp functionality works by performing a `setTransform` action on the Cell engine so that engine coordinates [0, 0] equal the entity's `currentStampPosition` coordinates, alongside any directionality (`flipReverse`, `flipUpend`) and rotational (`roll`) changes necessary
// + doing it this way saves a massive amount of calculation that is otherwise required to correctly position the entity in the display
P.rotateDestination = function (engine, x, y, entity) {

    let self = (entity) ? entity : this,
        mimic = self.mimic,
        pivot = self.pivot,
        reverse, upend,
        rotation = self.currentRotation;

    if (mimic && mimic.name && self.useMimicFlip) {

        reverse = (mimic.flipReverse) ? -1 : 1;
        upend = (mimic.flipUpend) ? -1 : 1;
    }
    else {

        reverse = (self.flipReverse) ? -1 : 1;
        upend = (self.flipUpend) ? -1 : 1;
    }

    if (mimic && mimic.name && self.useMimicRotation) {

        rotation = mimic.currentRotation;
    }
    else if (pivot && pivot.name && self.addPivotRotation) {

        rotation = pivot.currentRotation;
    }

    if (rotation) {

        rotation *= radian;

        let cos = Math.cos(rotation),
            sin = Math.sin(rotation);

        engine.setTransform((cos * reverse), (sin * reverse), (-sin * upend), (cos * upend), x, y);
    }
    else engine.setTransform(reverse, 0, 0, upend, x, y);

    return this;
};

// #### Cell pool
// A number of processes - for instance collision functionality, and applying filters to entitys and groups - require the use of a <canvas> element and its CanvasRenderingContext2D engine. Rather than generate these canvas elements on the fly, we store them in a pool, to help make the code more efficiant.
//
// To use a pool cell, request it using the exposed __requestCell__ function.
//
// IT IS IMPERATIVE that requested cells are released once work with them completes, using the __releaseCell__ function. Failure to do this leads to impaired performance as Javascript creates new canvas elements (often in multiples of 60 per second) which need to be garbage collected by the Javascript engine, thus leading to increasingly shoddy performance the longer the animation runs.
const cellPool = [];

P.poolDefs = {
    element: null,
    engine: null,
    state: null,
    width: 300,
    height: 100,
    alpha: 1,
    composite: 'source-over',
}

// `Exported function` - __requestCell__
const requestCell = function () {

    if (!cellPool.length) {

        cellPool.push(makeCell({
            name: `pool_${generateUniqueString()}`,
            isPool: true
        }));
    }

    let c = cellPool.shift();
    c.engine.save();

    return c;
};

// `Exported function` - __releaseCell__
const releaseCell = function (c) {

    if (c && c.type === 'Cell') {

        c.engine.restore();
        cellPool.push(c);
    }
};

// #### Factory
const makeCell = function (items) {

    if (!items) return false;
    return new Cell(items);
};

constructors.Cell = Cell;

// #### Exports
export {
    makeCell,
    requestCell,
    releaseCell,
};

redpajama_set_name: RedPajamaGithub