feat(core): basic page/edgeless toggle animation (#5283)

This commit is contained in:
Cats Juice
2023-12-19 08:48:53 +00:00
parent 4b0ca06d80
commit 55818539af
49 changed files with 6189 additions and 72 deletions

View File

@@ -0,0 +1,31 @@
import { keyframes, style } from '@vanilla-extract/css';
import { onboardingVars } from './style.css';
const fadeIn = keyframes({
from: { opacity: 0, pointerEvents: 'none' },
to: { opacity: 1, pointerEvents: 'auto' },
});
export const tooltip = style({
width: 500,
textAlign: 'center',
fontFamily: 'var(--affine-font-family)',
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textShadow: '0px 0px 4px rgba(66, 65, 73, 0.14)',
color: 'white',
opacity: 0,
animation: `${fadeIn} 1s ease forwards`,
animationDelay: onboardingVars.animateIn.tooltipShowUpDelay,
});
export const next = style({
position: 'absolute',
top: 0,
right: 0,
opacity: 0,
animation: `${fadeIn} 1s ease forwards`,
animationDelay: onboardingVars.animateIn.nextButtonShowUpDelay,
});

View File

@@ -0,0 +1,19 @@
import { Button } from '@affine/component';
import * as styles from './animate-in-tooltip.css';
export const AnimateInTooltip = ({ onNext }: { onNext: () => void }) => {
return (
<>
<div className={styles.tooltip}>
AFFiNE is a workspace with fully merged docs, <br />
whiteboards and databases
</div>
<div className={styles.next}>
<Button type="primary" size="extraLarge" onClick={onNext}>
Next
</Button>
</div>
</>
);
};

View File

@@ -0,0 +1,276 @@
import { CounterNote } from '../switch-widgets/counter-note';
import type { OnboardingBlockOption } from '../types';
import bookmark1png from './assets/article-0-bookmark-1.png';
import bookmark2png from './assets/article-0-bookmark-2.png';
import embed1png from './assets/article-0-embed-1.png';
export const article0: Array<OnboardingBlockOption> = [
{
children: <h1>HOWTO: Be more productive</h1>,
offset: { x: -150, y: 0 },
},
{
bg: '#f5f5f5',
children: (
<>
<p>
With all the time you spend watching TV, he tells me, you could
have written a novel by now. Its hard to disagree with the sentiment
writing a novel is undoubtedly a better use of time than watching TV
but what about the hidden assumption? Such comments imply that time
is fungible that time spent watching TV can just as easily be
spent writing a novel. And sadly, thats just not the case.
</p>
<p>
Time has various levels of quality. If Im walking to the subway
station and Ive forgotten my notebook, then its pretty hard for me
to write more than a couple paragraphs. And its tough to focus when
you keep getting interrupted. Theres also a mental component:
sometimes I feel happy and motivated and ready to work on something,
but other times I feel so sad and tired I can only watch TV.
</p>
</>
),
offset: { x: -120, y: 80 },
sub: {
children: (
<CounterNote
index={1}
width={290}
label="Time isn't interchangeable; it varies in quality due to circumstances and mental states."
animationDelay={300}
color="#6E52DF"
/>
),
enterDelay: 300,
position: {},
style: {
bottom: 'calc(100% + 20px)',
left: -40,
},
edgelessOnly: true,
},
},
{
bg: '#F9E8FF',
children: (
<>
<img draggable={false} width="100%" src={bookmark1png} />
<p>
If you want to be more productive then, you have to recognize this
fact and deal with it. First, you have to make the best of each kind
of time. And second, you have to try to make your time higher-quality.
</p>
<h3>Spend time efficiently</h3>
</>
),
offset: { x: 250, y: 100 },
},
{
bg: '#E1EFFF',
children: (
<>
<h2>Choose good problems</h2>
<p>
Life is short (or so Im told) so why waste it doing something dumb?
Its easy to start working on something because its convenient, but
you should always be questioning yourself about it. Is there something
more important you can work on? Why dont you do that instead? Such
questions are hard to face up to (eventually, if you follow this rule,
youll have to ask yourself why youre not working on the most
important problem in the world) but each little step makes you more
productive.
</p>
<p>
<em style={{ background: '#ADF8E9' }}>
This isnt to say that all your time should be spent on the most
important problem in the world. Mine certainly isnt (after all, Im
writing this essay). But its definitely the standard against which
I measure my life.
</em>
</p>
</>
),
offset: { x: -600, y: -130 },
sub: {
children: (
<CounterNote
index={2}
width={290}
label="Prioritize, question, and work towards productivity."
animationDelay={800}
color="#6E52DF"
/>
),
edgelessOnly: true,
enterDelay: 800,
position: {},
style: { bottom: 'calc(100% + 20px)', left: -40 },
},
},
{
bg: '#DFF4E8',
children: (
<>
<h2>Have a bunch of them</h2>
<p>
Another common myth is that youll get more done if you pick one
problem and focus on it exclusively. I find this is hardly ever true.
Just this moment for example, Im trying to fix my posture, exercise
some muscles, drink some fluids, clean off my desk, IM with my
brother, and write this essay. Over the course the day, Ive worked on
this essay, read a book, had some food, answered some email, chatted
with friends, done some shopping, worked on a couple other essays,
backed up my hard drive, and organized my book list. In the past week
Ive worked on several different software projects, read several
different books, studied a couple different programming languages,
moved some of my stuff, and so on.
</p>
<p>
Having a lot of different projects gives you work for different
qualities of time. Plus, youll have other things to work on if you
get stuck or bored (and that can give your mind time to unstick
yourself).
</p>
<p>
It also makes you more creative. Creativity comes from applying things
you learn in other fields to the field you work in. If you have a
bunch of different projects going in different fields, then you have
many more ideas you can apply.
</p>
</>
),
offset: { x: -50, y: -50 },
sub: {
children: (
<CounterNote
index={3}
width={290}
label="Diverse tasks enhance productivity, creativity, and combat boredom effectively."
animationDelay={1200}
color="#6E52DF"
/>
),
edgelessOnly: true,
enterDelay: 1200,
position: {},
style: { bottom: 'calc(100% + 20px)', left: -40 },
},
},
{
bg: '#DFF4F3',
children: (
<>
<h2>Make a list</h2>
<p>
Coming up with a bunch of different things to work on shouldnt be
hard most people have tons of stuff they want to get done. But if
you try to keep it all in your head it quickly gets overwhelming. The
psychic pressure of having to remember all of it can make you crazy.
The solution is again simple: write it down.
</p>
<p>
Once you have a list of all the things you want to do, you can
organize it by kind. For example, my list is programming, writing,
thinking, errands, reading, listening, and watching (in that order).
</p>
<p>
Most major projects involve a bunch of these different tasks. Writing
this, for example, involves reading about other procrastination
systems, thinking up new sections of the article, cleaning up
sentences, emailing people with questions, and so on, all in addition
to the actual work of writing the text. Each task can go under the
appropriate section, so that you can do it when you have the right
kind of time.
</p>
</>
),
offset: { x: 800, y: -400 },
sub: {
children: (
<CounterNote
index={4}
width={290}
label="Organize tasks by category to manage overwhelming to-do lists efficiently."
animationDelay={1500}
color="#6E52DF"
/>
),
edgelessOnly: true,
enterDelay: 1500,
position: {},
style: { bottom: 'calc(100% + 20px)', left: -40 },
},
},
{
// children: <Article0Bookmark2 />,
children: <img draggable={false} width={418} src={bookmark2png} />,
edgelessOnly: true,
position: { x: 700, y: 230 },
fromPosition: { x: 1000, y: 0 },
},
{
bg: '#FFE1E1',
children: (
<>
<h2>Integrate the list with your life</h2>
<p>
Once you have this list, the problem becomes remembering to look at
it. And the best way to remember to look at it is to make looking at
it what you would do anyway. For example, I keep a stack of books on
my desk, with the ones Im currently reading on top. When I need a
book to read, I just grab the top one off the stack.
</p>
<p>
I do the same thing with TV/movies. Whenever I hear about a movie I
should watch, I put it in a special folder on my computer. Now
whenever I feel like watching TV, I just open up that folder.
</p>
<p>
Ive also thought about some more intrusive ways of doing this. For
example, a web page that pops up with a list of articles in my to
read folder whenever I try to check some weblogs. Or maybe even a
window that pops up with work suggestions occasionally for me to see
when Im goofing off.
</p>
<p>
Making the best use of the time you have can only get you so far. The
much more important problem is making more higher quality time for
yourself. Most peoples time is eaten up by things like school and
work. Obviously if you attend one of these, you should stop. But what
else can you do?
</p>
</>
),
offset: { x: 1200, y: -1600 },
sub: {
children: (
<CounterNote
index={5}
width={290}
label="Integrate tasks into daily routines, create more high-quality free time."
animationDelay={1500}
color="#6E52DF"
/>
),
edgelessOnly: true,
enterDelay: 1500,
position: {},
style: { bottom: 'calc(100% + 20px)', left: -40 },
},
},
{
// children: <Article0Embed1 />,
children: <img draggable={false} width={450} src={embed1png} />,
edgelessOnly: true,
position: { x: 1050, y: 630 },
fromPosition: { x: 1400, y: 630 },
enterDelay: 200,
},
];

View File

@@ -0,0 +1,294 @@
import clsx from 'clsx';
import { CounterNote } from '../switch-widgets/counter-note';
import { PageLink } from '../switch-widgets/page-link';
import type { OnboardingBlockOption } from '../types';
import bookmark1png from './assets/article-1-bookmark-1.png';
import illustration1png from './assets/article-1-illustration-1.png';
import Article1Illustration2 from './assets/article-1-illustration-2';
import { hr, link, quote } from './blocks.css';
export const article1: Array<OnboardingBlockOption> = [
{
children: <h1>This is Local-first software</h1>,
offset: { x: -600, y: 0 },
},
{
bg: '#F5F5F5',
children: (
<>
<h2>Local-first software</h2>
<h3>You own your data, in spite of the cloud</h3>
<p>
Cloud apps like <a className={link}>Google Docs</a> and{' '}
<a className={link}>Trello</a> are popular because they enable
real-time collaboration with colleagues, and they make it easy for us
to access our work from all of our devices. However, by centralizing
data storage on servers, cloud apps also take away ownership and
agency from users.{' '}
<b>
If a service shuts down, the software stops functioning, and data
created with that software is lost.
</b>
</p>
</>
),
offset: { x: -570, y: 80 },
sub: {
children: (
<CounterNote
index={1}
width={500}
label="Cloud apps enable collaboration but can jeopardize data ownership; time varies."
animationDelay={300}
color="#E660A4"
/>
),
edgelessOnly: true,
enterDelay: 300,
position: {},
style: { bottom: 'calc(100% + 20px)', left: -40 },
},
},
{
bg: '#F3F0FF',
children: (
<>
<img draggable={false} width="100%" src={bookmark1png} />
<p className={clsx(quote)}>
If you are an entrepreneur interested in building developer
infrastructure, all of the above suggests an interesting market
opportunity: Firebase for CRDTs.
</p>
<p>
In this article we propose <PageLink>local-first software</PageLink>{' '}
of principles for software that enables both collaboration
</p>
</>
),
offset: { x: -570, y: 200 },
sub: {
children: (
<CounterNote
index={2}
width={300}
label="Local-first software emphasizes collaboration, ownership, and data control for users."
animationDelay={600}
color="#E660A4"
/>
),
edgelessOnly: true,
enterDelay: 600,
position: {},
style: { bottom: 'calc(100% + 20px)', left: -40 },
},
},
{
bg: '#DFF4F3',
children: (
<>
<p>
We survey existing approaches to data storage and sharing, ranging
from email attachments to web apps to Firebase-backed mobile apps, and
we examine the trade-offs of each. We look at Conflict-free Replicated
Data Types (CRDTs): data structures that are multi-user from the
ground up while also being fundamentally local and private. CRDTs have
the potential to be a foundational technology for realizing
local-first software.
</p>
<hr className={hr} />
<p>
We share some of our findings from developing local-first software
prototypes at <a className={link}>Ink & Switch</a> over the course of
several years. These experiments test the viability of CRDTs in
practice, and explore the user interface challenges for this new data
model. Lastly, we suggest some next steps for moving towards
local-first software: for researchers, for app developers, and a
startup opportunity for entrepreneurs.
</p>
</>
),
offset: { x: 290, y: -140 },
sub: {
children: (
<CounterNote
index={3}
width={300}
label="Examining data storage, CRDTs' role, prototypes, and future possibilities."
animationDelay={900}
color="#E660A4"
/>
),
edgelessOnly: true,
enterDelay: 900,
position: {},
style: { bottom: 'calc(100% + 20px)', left: -40 },
},
},
{
bg: '#FFF4D8',
children: (
<>
<p>
This article has also been <a className={link}>published in PDF</a>{' '}
format in the proceedings of the{' '}
<a className={link}>Onward! 2019 conference</a>. Please cite it as:
</p>
<p className={clsx(quote)}>
Martin Kleppmann, Adam Wiggins, Peter van Hardenberg, and Mark
McGranaghan. Local-first software: you own your data, in spite of the
cloud. 2019 ACM SIGPLAN International Symposium on New Ideas, New
Paradigms, and Reflections on Programming and Software (Onward!),
October 2019, pages 154178.{' '}
<a className={link}>doi:10.1145/3359591.3359737</a>
</p>
<p>
We welcome your feedback: <a className={link}>@inkandswitch</a> or
<a className={link}>hello@inkandswitch.com</a>.
</p>
</>
),
offset: { x: 350, y: -850 },
},
{
bg: '#E1EFFF',
children: (
<>
<h2>Contents</h2>
<h3>
Motivation: collaboration and ownership.
<br />
Seven ideals for local-first software
</h3>
<ol>
<li>No spinners: your work at your fingertips</li>
<li>
<a className={link}>Your work is not trapped on one device</a>
</li>
<li>
<PageLink>The network is optional</PageLink>
</li>
<li>Seamless collaboration with your colleagues</li>
<li>
<PageLink>The Long Now</PageLink>
</li>
<li>Security and privacy by default</li>
<li>You retain ultimate ownership and control</li>
</ol>
<h3>Existing data storage and sharing models</h3>
<ul>
<li>How application architecture affects user experience</li>
<li>Developer infrastructure for building apps</li>
</ul>
<h3>Towards a better future</h3>
<ul>
<li>CRDTs as a foundational technology</li>
<li>Ink & Switch prototypes</li>
<li>How you can help</li>
</ul>
<h3>Conclusions</h3>
<ul>
<li>Acknowledgments</li>
</ul>
</>
),
offset: { x: 300, y: -250 },
customStyle: { edgeless: { width: 500 } },
sub: {
children: (
<CounterNote
index={4}
width={400}
label="Motivation, ideals, existing models, architecture, CRDTs, prototypes, future, help, conclusions."
animationDelay={1200}
color="#E660A4"
/>
),
edgelessOnly: true,
enterDelay: 1200,
position: {},
style: { bottom: 'calc(100% + 20px)', left: -40 },
},
},
{
bg: '#FFE1E1',
children: (
<>
<h3>Motivation: collaboration and ownership</h3>
<p>
Its amazing how easily we can collaborate online nowadays. We use
Google Docs to collaborate on documents, spreadsheets and
presentations; in Figma we work together on user interface designs; we
communicate with colleagues using Slack; we track tasks in Trello; and
so on. We depend on these and many other online services, e.g. for
taking notes, planning projects or events, remembering contacts, and a
whole raft of business uses.
</p>
<p>
Todays cloud apps offer big benefits compared to earlier generations
of software: seamless collaboration, and being able to access data
from any device. As we run more and more of our lives and work through
these cloud apps, they become more and more critical to us. The more
time we invest in using one of these apps, the more valuable the data
in it becomes to us.
</p>
<p>
However, in our research we have spoken to a lot of creative
professionals, and in that process we have also learned about the
downsides of cloud apps.
</p>
<p>
When you have put a lot of creative energy and effort into making
something, you tend to have a deep emotional attachment to it. If you
do creative work, this probably seems familiar. (When we say creative
work, we mean not just visual art, or music, or poetry many other
activities, such as explaining a technical topic, implementing an
intricate algorithm, designing a user interface, or figuring out how
to lead a team towards some goal are also creative efforts.)
</p>
</>
),
offset: { x: 900, y: -950 },
sub: {
children: (
<CounterNote
index={5}
width={400}
label="Online collaboration's benefits but emotional attachment and downsides discussed."
animationDelay={1500}
color="#E660A4"
/>
),
edgelessOnly: true,
enterDelay: 1500,
position: {},
style: { bottom: 'calc(100% + 20px)', left: -40 },
},
},
{
children: <img width={784} draggable={false} src={illustration1png} />,
edgelessOnly: true,
position: { x: -600, y: 1000 },
fromPosition: { x: -1000, y: 1500 },
enterDelay: 250,
},
{
children: <Article1Illustration2 />,
edgelessOnly: true,
position: { x: 1200, y: 500 },
fromPosition: { x: 1800, y: -100 },
enterDelay: 200,
},
];

View File

@@ -0,0 +1,174 @@
import type { OnboardingBlockOption } from '../types';
import embed1png from './assets/article-2-embed-1.png';
import illustration1png from './assets/article-2-illustration-1.jpg';
import illustration2png from './assets/article-2-illustration-2.jpg';
import note1png from './assets/article-2-note-1.png';
import note2png from './assets/article-2-note-2.png';
export const article2: Array<OnboardingBlockOption> = [
{
children: (
<h1>
Learning with earning with
<br /> retrieval practice
</h1>
),
offset: { x: -824, y: 0 },
},
{
bg: '#DFF4E8',
children: (
<h2>
Are there any specific techniques to make the process of learning more
effective?
</h2>
),
offset: { x: -800, y: 100 },
},
{
bg: '#DFF4E8',
children: (
<>
<p>
Students often re-read, underline, or highlight materials, thinking
that it will help them learn better. But, the best method for really
turning information into long-term memory is to use what is called
retrieval practice.
</p>
<img
className="illustration"
draggable={false}
width="100%"
src={illustration1png}
/>
<p>
It simply means trying to retrieve the information from your own
brain, instead of looking at a sheet of paper. When you have really
learnt something, youve created sets of links between the neurons in
long-term memory. Each time you tug on those sets of links and bring
them back to mind from your memory, you strengthen them. This is why,
using flashcards, teaching others, or trying to retrieve key ideas
from your own mind, after having glanced at a page or your notes, can
be valuable.
</p>
</>
),
offset: { x: -800, y: 100 },
},
{
bg: '#FFF4D8',
children: <h2>How can a student learn more effectively?</h2>,
offset: { x: 100, y: -300 },
},
{
bg: '#FFF4D8',
children: (
<>
<p>
The best method for more efficient learning is to avoid multitasking.
The Pomodoro Technique helps with this. To do a Pomodoro, just put
away all distractions (no pop-upson your computer or dings from your
phone!), set a timer for 25 minutes, and focus as fully as you can for
those 25 minutes, bringing your thoughts back to the task if you find
them wandering. At the end of the 25 minutes, give yourself a
five-minutes break and do not indulge in anything where you have to
focus on (no checking e-mails, for example but making a cup of tea
is just fine). Repeat the Pomodoro/break three or four times, and
then, take a 30-minutes break or wrap your studies up for the day!
</p>
<img
className="illustration"
draggable={false}
width="100%"
src={illustration2png}
/>
</>
),
offset: { x: 100, y: -300 },
},
{
bg: '#DFF4F3',
children: (
<>
<h2>How can students remember more and forget less?</h2>
<p>
We all wish we had much better memories, but, if you remember too
perfectly, it makes it harder to revisit and update your learning to
be more flexible on the face of new information, or to change and
adapt if youve got something wrong. If you want to remember more,
retrieval practice with spaced repetition, that is, spacing your
retrieval practice out over a number of days is your best bet.
Researchers sug-gest that you should retrieve the information when you
are about to forget it, which is a tricky bit of timing to figure out.
</p>
<h2>How can we make online learning more effective for students?</h2>
<p>
It can be hard to write a textbook for a class; so, most professors
use professional textbooks to supplement their teachings. Similarly,
it can be hard to create high-quality video materials. I believe
online teaching will continue to become more common post-COVID the
genie is out of the bottle as, now, students realise that online
learning can be more convenient than face-to-face classes. But,
students still like their teachers real professors, who they can
interact with. So, real face-to-face instructors will always have a
place.
</p>
</>
),
offset: { x: -750, y: -530 },
},
{
bg: '#F3F0FF',
children: (
<>
<h2>
Do you think students should use social media, like YouTube, for
learning?
</h2>
<p>
There are some excellent learning materials available on YouTube, but,
students may have to spend a lot of time wading through the dreck to
find what they are looking for. Students and all of us need to be
a little wary of spending too much time on social media. It can be the
equivalent of parking yourself in front of the television and
indulging in hours of mindless entertainment except that social
media can also be an echo chamber that can push us to unwittingly
become more extreme in our beliefs.
</p>
</>
),
offset: { x: 150, y: -680 },
},
{
children: <img draggable={false} width={380} src={embed1png} />,
edgelessOnly: true,
position: { x: -300, y: 0 },
fromPosition: { x: 300, y: -300 },
},
{
children: <img draggable={false} width={309} src={note1png} />,
edgelessOnly: true,
position: { x: -360, y: -20 },
fromPosition: { x: -360, y: -100 },
enterDelay: 300,
customStyle: {
page: {
transform: 'rotate(-10deg) translateY(-100px)',
},
},
},
{
children: <img draggable={false} width={1800} src={note2png} />,
edgelessOnly: true,
position: { x: 0, y: 0 },
fromPosition: { x: 2000, y: -2000 },
},
];

View File

@@ -0,0 +1,198 @@
import type { OnboardingBlockOption } from '../types';
import bookmark1png from './assets/article-3-bookmark-1.png';
import illustration1jpg from './assets/article-3-illustration-1.jpg';
import illustration2jpg from './assets/article-3-illustration-2.jpg';
import illustration3jpg from './assets/article-3-illustration-3.jpg';
import illustration4jpg from './assets/article-3-illustration-4.jpg';
import illustration5jpg from './assets/article-3-illustration-5.jpg';
export const article3: Array<OnboardingBlockOption> = [
{
children: (
<img
className="illustration"
draggable={false}
width={290}
src={illustration5jpg}
/>
),
edgelessOnly: true,
position: { x: -780, y: 216 },
fromPosition: { x: -1500, y: 216 },
enterDelay: 200,
},
{
children: <img draggable={false} width={450} src={bookmark1png} />,
edgelessOnly: true,
position: { x: 500, y: 200 },
fromPosition: { x: 1000, y: -200 },
enterDelay: 200,
leaveDelay: 100,
},
{
children: <h1>Breath of the Wild: Redefining Game Design</h1>,
offset: { x: -400, y: 0 },
customStyle: {
edgeless: { whiteSpace: 'nowrap' },
},
},
{
bg: '#E1EFFF',
children: (
<>
<h2>Introduction</h2>
<p>
At GDC 2017, Hidemaro Fujibayashi, Satoru Takizawa, and Takuhiro Dohta
from Nintendo shared their insights on The Legend of Zelda: Breath of
the Wild&apos;s groundbreaking game mechanics. One standout feature is
the &quot;multiplicative gameplay,&quot; which empowers players to
interact with the game world in diverse ways, leading to surprising
outcomes.
</p>
<h2>Mechanics</h2>
<p>
Multiplicative gameplay works like a magical concoction, where
players&apos; actions, terrain, and items combine to create a range of
unexpected results. For instance, players can set trees ablaze to
create a fiery barrier against enemies, detonate bombs to carve new
paths, or soar through the skies with a glider for a bird&apos;s-eye
view of the world.
</p>
<p>
To achieve multiplicative gameplay, the development team made
significant changes to the game&apos;s terrain system, physics system,
and action system.
</p>
</>
),
offset: { x: -400, y: 0 },
},
{
bg: '#F5F5F5',
children: (
<>
<h2>Terrain</h2>
<p>
The terrain system enables players to climb any surface, whether
it&apos;s a wall, a tree, or a rock, granting them the freedom to
explore every nook and cranny of the game world.
</p>
<img
className="illustration"
draggable={false}
width="100%"
src={illustration1jpg}
/>
</>
),
offset: { x: 480, y: -250 },
},
{
bg: '#FFEACA',
children: (
<>
<h2>Physics</h2>
<p>
Unlike traditional games, the physics system in Breath of the Wild is
more open, allowing players to leverage natural phenomena to solve
puzzles or create new gameplay opportunities. For instance, players
can ignite trees to create barriers of fire or use bombs to open up
new pathways.
</p>
<img
className="illustration"
draggable={false}
width="100%"
src={illustration2jpg}
/>
</>
),
offset: { x: -500, y: -400 },
},
{
bg: '#DFF4E8',
children: (
<>
<h2>Action</h2>
<p>
The action system offers more flexibility than in traditional games,
empowering players to choose how they want to interact with the game
world. Whether it&apos;s wielding a sword, a shield, or a bow and
arrow, players have the freedom to approach challenges in their own
unique way.
</p>
<img
className="illustration"
draggable={false}
width="100%"
src={illustration3jpg}
/>
</>
),
offset: { x: 400, y: -700 },
},
{
bg: '#FFE1E1',
children: (
<>
<h2>Validation</h2>
<p>
Takizawa also outlined the process for validating multiplicative
gameplay. He believes that it can be achieved through the following
steps:
</p>
<img
className="illustration"
draggable={false}
width="100%"
src={illustration4jpg}
/>
<p>
List all possible behaviors, terrain features, and items. For example,
in Breath of the Wild, players can engage in activities such as
climbing, gliding, swimming, and combat; terrain includes mountains,
forests, plains, and lakes; and items encompass weapons, tools, and
food.
</p>
<p>
Analyze the interactions between these elements. For example, players
can use fire to ignite trees, creating a barrier of fire to impede
enemies&apos; progress.
</p>
<p>
Test the results of these interactions. For example, the development
team can assess whether players can safely navigate through a wall of
fire.
</p>
<p>
By following these steps, developers can verify whether multiplicative
gameplay will yield the desired outcomes.
</p>
</>
),
offset: { x: -440, y: -870 },
},
{
bg: '#F3F0FF',
children: (
<>
<h2>Conclusion</h2>
<p>
Multiplicative gameplay is a potent tool for crafting more captivating
and immersive gaming experiences. It empowers players to explore the
game world in creative and flexible ways, leading to fresh and
unexpected discoveries.
</p>
</>
),
offset: { x: 450, y: -1400 },
},
];

View File

@@ -0,0 +1,247 @@
import { ShadowSticker } from '../switch-widgets/shadow-sticker';
import type { OnboardingBlockOption } from '../types';
import bookmark1png from './assets/article-4-bookmark-1.png';
import bookmark2png from './assets/article-4-bookmark-2.png';
import illustration1jpg from './assets/article-4-illustration-1.jpg';
import illustration2jpg from './assets/article-4-illustration-2.jpg';
export const article4: Array<OnboardingBlockOption> = [
{
children: <h1>More Is Different</h1>,
offset: { x: -430, y: 0 },
},
{
bg: '#FFEACA',
offset: { x: -400, y: 0 },
children: (
<>
<h2>
Broken symmetry and the nature of the hierarchical structure of
science
</h2>
<img draggable={false} width="100%" src={bookmark1png} />
<p>
The reductionist hypothesis may still be a topic for controversy among
philosophers, but among the great majority of active scientists I
think it is accepted without questions. The workings of our minds and
bodies, and of all the animate or inanimate matter of which we have
any detailed knowledge, are assumed to be controlled by the same set
of fundamental laws, which except under certain extreme conditions we
feel we know pretty well.
</p>
<p>
It seems inevitable to go on uncritically to what appears at first
sight to be an obvious corollary of reductionism: that if everything
obeys the same fundamental laws, then the only scientists who are
studying anything really fundamental are those who are working on
those laws. In practice, that amounts to some astrophysicists, some
elementary particle physicists, some logicians and other
mathematicians, and few others. This point of view, which it is the
main purpose of this article to oppose, is expressed in a rather
well-known passage by Weisskopf (1):
</p>
</>
),
sub: {
children: (
<ShadowSticker width={300}>
Reductionist hypothesis accepted by most scientists, fundamental laws
in focus.
</ShadowSticker>
),
edgelessOnly: true,
position: {},
style: {
right: -20,
bottom: '100%',
transformOrigin: '100% 100%',
},
customStyle: {
page: { transform: 'scale(0)' },
edgeless: {},
},
enterDelay: 200,
leaveDelay: 100,
},
},
{
bg: '#DFF4F3',
offset: { x: 500, y: -490 },
children: (
<p>
Looking at the development of science in the Twentieth Century one can
distinguish two trends, which I will call intensive and extensive
research, lacking a better terminology. In short: intensive research
goes for the fundamental laws, extensive research goes for the
explanation of phenomena in terms of known fundamental laws, As always,
distinctions of this kind are not unambiguous, but they are clear in
most cases. Solid state physics plasma physics, and perhaps also biology
are extensive. High energy physics and a good part of nuclear physics
are intensive research going on than extensive. Once new fundamental
laws are discovered, a large and ever increasing activity begins in
order to apply the discoveries to hitherto unexplained phenomena. Thus,
there are two dimensions to basic research. The frontier of science
extends all along a long line from the newest and most modern intensive
research, over the extensive research recently spawned by the intensive
research of yesterday, to the broad and well developed web of extensive
research activities based on intensive research of past decades
</p>
),
sub: {
children: (
<ShadowSticker width={300}>
Twentieth Century science: intensive vs. extensive research,
fundamental laws&apos; impact.
</ShadowSticker>
),
position: {},
style: {
left: 'calc(100% - 50px)',
bottom: '0px',
transformOrigin: '0% 50%',
},
customStyle: {
page: { transform: 'scale(0)' },
edgeless: {},
},
enterDelay: 300,
leaveDelay: 100,
},
},
{
bg: '#E1EFFF',
offset: { x: -800, y: -280 },
children: (
<>
<p>
The effectiveness of this message may be indicated by the face that I
heard it quoted recently by a leader in the field of materials
science, who urged the participants at a meeting dedicated to
fundamental problems in condensed matter physics to accept that
there were few or no such problems and that nothing was left but
extensive science, which he seemed to equate with device engineering.
</p>
<p>
The main fallacy in this kind of thinking is that the reductionist
hypothesis does not by any means imply a constructions one: The
ability to reduce everything to simple fundamental laws does not imply
the ability to start from those laws and reconstruct the universe. In
fact, the more the elementary particle physicists tell us about the
nature of the fundamental laws, the less relevance they seem to have
to the very real problems of the rest of science much less to those of
society.
</p>
</>
),
sub: {
children: (
<ShadowSticker width={336}>
Misunderstanding: Reductionism doesn&apos;t mean reconstructing
complex phenomena from fundamentals.
</ShadowSticker>
),
position: {},
style: {
top: 'calc(100% - 30px)',
left: 'calc(100% - 250px)',
transformOrigin: '0 0',
},
customStyle: {
page: { transform: 'scale(0)' },
edgeless: {},
},
enterDelay: 400,
leaveDelay: 100,
},
},
{
bg: '#FFE1E1',
offset: { x: 580, y: -680 },
children: (
<>
<p>
The constructionist hypothesis breaks down when confronted with the
twin difficulties of scale and complexity. The behavior of large and
complex aggregates of elementary particles, it turns out, is not to be
understood in terms of a simple extrapolation entirely new properties
appear, and the understanding of the new behaviors requires research
which I think is as fundamental in its nature as any other. That is,
it seems to me that one may array the sciences roughly linearly in a
hierarchy, according to the idea: The elementary entities of science X
obey the laws of science Y.
</p>
<p>
But this hierarchy does not imply that science X is just applied Y.
At each stage entirely new laws, concepts and generalizations are
necessary, requiring inspiration and creativity to just as great a
degree as in the previous one. Psychology is not applied biology, nor
is biology applied chemistry.
</p>
</>
),
sub: {
children: (
<ShadowSticker width={463}>
Complex systems introduce new properties, demanding fundamental
research beyond reductionism.
</ShadowSticker>
),
edgelessOnly: true,
position: {},
style: {
bottom: '100%',
left: '-100px',
transformOrigin: '0% 100%',
},
customStyle: {
page: { transform: 'scale(0)' },
edgeless: {},
},
enterDelay: 500,
leaveDelay: 100,
},
},
//
{
children: <img draggable={false} width={500} src={bookmark2png} />,
edgelessOnly: true,
position: { x: 0, y: 760 },
},
{
children: (
<img
className="illustration"
draggable={false}
width={322}
src={illustration1jpg}
/>
),
edgelessOnly: true,
position: { x: -820, y: 150 },
fromPosition: { x: -1800, y: 150 },
enterDelay: 200,
leaveDelay: 200,
sub: {
children: (
<img
className="illustration"
draggable={false}
width={213}
src={illustration2jpg}
/>
),
edgelessOnly: true,
position: {},
style: {
top: 'calc(100% - 40px)',
left: 'calc(100% - 250px)',
},
},
},
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -0,0 +1,137 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const block = style({});
globalStyle(`${block} h1`, {
fontSize: '40px',
fontWeight: '600',
lineHeight: '48px',
});
globalStyle(`${block} h2`, {
fontSize: '20px',
fontWeight: '600',
lineHeight: '28px',
});
globalStyle(`${block} h3`, {
fontSize: '18px',
fontWeight: '600',
lineHeight: '26px',
});
globalStyle(`${block} p`, {
fontSize: '15px',
fontWeight: 400,
lineHeight: '23px',
});
globalStyle(`${block} b`, {
// TODO: 500's effect not matching the design, use 600 for now
fontWeight: '600',
});
globalStyle(`${block} ol`, {
counterReset: 'section',
listStyleType: 'none',
display: 'flex',
flexDirection: 'column',
gap: '2px',
});
globalStyle(`${block} ol li, ${block} ul li`, {
fontSize: '15px',
fontWeight: 400,
lineHeight: '23px',
});
globalStyle(`${block} ol li::before`, {
display: 'inline-block',
counterIncrement: 'section',
content: 'counter(section) "."',
color: '#1C81D9',
width: '24px',
marginRight: '4px',
});
globalStyle(`${block} ul`, {
listStyleType: 'none',
display: 'flex',
flexDirection: 'column',
gap: '2px',
});
globalStyle(`${block} ul li`, {
position: 'relative',
paddingLeft: '24px',
});
globalStyle(`${block} ul li::before`, {
vars: {
'--dot-svg': `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Ccircle cx='2' cy='2' r='2' fill='%231C81D9'/%3E%3C/svg%3E")`,
},
content: '""',
position: 'absolute',
left: 0,
top: 0,
width: '24px',
height: '24px',
display: 'inline-block',
backgroundImage: 'var(--dot-svg)',
backgroundRepeat: 'no-repeat',
backgroundPosition: '4px 50%',
});
globalStyle(`${block} img.illustration`, {
borderRadius: '5px',
overflow: 'hidden',
});
export const link = style({
color: '#1E67AF',
});
export const pageLink = style({
color: '#424149',
fontWeight: '500',
display: 'inline-flex',
alignItems: 'baseline',
gap: '5px',
});
export const pageLinkIcon = style({
color: '#77757D',
alignSelf: 'center',
});
export const pageLinkLabel = style({
position: 'relative',
selectors: {
'&::after': {
content: '""',
position: 'absolute',
bottom: '-1px',
left: 0,
height: '1px',
transform: 'scaleY(0.5)',
width: '100%',
backgroundColor: '#E3E2E4',
},
},
});
export const quote = style({
paddingLeft: '17px',
position: 'relative',
selectors: {
'&::before': {
position: 'absolute',
content: '""',
top: 0,
left: 0,
height: '100%',
width: '2px',
borderRadius: '1px',
backgroundColor: '#D9D9D9',
},
},
});
export const hr = style({
height: '1px',
backgroundColor: '#E3E2E4',
border: 'none',
margin: '18px 0',
width: 'calc(100% - 20px)',
alignSelf: 'center',
});

View File

@@ -1,5 +1,11 @@
import { article, articleWrapper, text, title } from '../curve-paper/paper.css';
import type { ArticleId, ArticleOption } from '../types';
import type { ArticleId, ArticleOption, EdgelessSwitchState } from '../types';
// TODO: lazy load
import { article0 } from './article-0';
import { article1 } from './article-1';
import { article2 } from './article-2';
import { article3 } from './article-3';
import { article4 } from './article-4';
const ids = ['0', '1', '2', '3', '4'] as Array<ArticleId>;
@@ -127,19 +133,29 @@ const paperBriefs = {
'0': (
<div className={articleWrapper}>
<article className={article}>
<h1 className={title}>Breath of the Wild: Redefining Game Design</h1>
<h1 className={title}>HOWTO: Be more productive</h1>
<p className={text}>
With all the time you spend watching TV, he tells me, you could
have written a novel by now. Its hard to disagree with the sentiment
writing a novel is undoubtedly a better use of time than watching TV
but what about the hidden assumption? Such comments imply that time
is fungible that time spent watching TV can just as easily be
spent writing a novel. And sadly, thats just not the case.
but what about the hidden ...
</p>
</article>
</div>
),
'1': (
'3': (
<div className={articleWrapper}>
<article className={article}>
<h1 className={title}>Breath of the Wild: Redefining Game Design</h1>
<p className={text}>
At GDC 2017, Hidemaro Fujibayashi, Satoru Takizawa, and Takuhiro Dohta
from Nintendo shared their insights on The Legend of Zelda: Breath of
the Wild&apos;s groundbreaking game mechanics. One standout ...
</p>
</article>
</div>
),
'2': (
<div className={articleWrapper}>
<article className={article}>
<h1 className={title}>Learning with earning with retrieval practice</h1>
@@ -150,13 +166,12 @@ const paperBriefs = {
<p className={text}>
Students often re-read, underline, or highlight materials, thinking
that it will help them learn better. But, the best method for really
turning information into long-term memory is to use what is called
retrieval practice.
...
</p>
</article>
</div>
),
'2': (
'1': (
<div className={articleWrapper}>
<article className={article}>
<h1 className={title}>
@@ -168,14 +183,12 @@ const paperBriefs = {
Cloud apps like Google Docs and Trello are popular because they enable
real-time collaboration with colleagues, and they make it easy for us
to access our work from all of our devices. However, by centralizing
data storage on servers, cloud apps also take away ownership and
agency from users. If a service shuts down, the software stops
functioning, and data created with that software is lost.
...
</p>
</article>
</div>
),
'3': (
'4': (
<div className={articleWrapper}>
<article className={article}>
<h1 className={title}>More Is Different</h1>
@@ -186,32 +199,49 @@ const paperBriefs = {
<p className={text}>
The reductionist hypothesis may still be a topic for controversy among
philosophers, but among the great majority of active scientists I
think it is accepted without questions. The workings of our minds and
bodies, and of all the animate or inanimate matter of which we have
any detailed knowledge, are assumed to be controlled by the same set
of fundamental laws, which except under certain extreme conditions we
feel we know pretty well.
</p>
</article>
</div>
),
'4': (
<div className={articleWrapper}>
<article className={article}>
<h1 className={title}>HOWTO: Be more productive</h1>
<p className={text}>
With all the time you spend watching TV, he tells me, you could
have written a novel by now. Its hard to disagree with the sentiment
writing a novel is undoubtedly a better use of time than watching TV
but what about the hidden assumption? Such comments imply that time
is fungible that time spent watching TV can just as easily be
spent writing a novel. And sadly, thats just not the case.
think it is accepted without ...
</p>
</article>
</div>
),
};
const contents = {
'0': article0,
'1': article1,
'2': article2,
'3': article3,
'4': article4,
};
const states: Partial<Record<ArticleId, EdgelessSwitchState>> = {
'0': {
scale: 0.5,
offsetX: -330,
offsetY: -380,
},
'1': {
scale: 0.4,
offsetX: -330,
offsetY: -500,
},
'2': {
scale: 0.45,
offsetX: 0,
offsetY: -380,
},
'3': {
scale: 0.4,
offsetX: 100,
offsetY: -320,
},
'4': {
scale: 0.48,
offsetX: 10,
offsetY: -220,
},
};
export const articles: Record<ArticleId, ArticleOption> = ids.reduce(
(acc, id) => {
return {
@@ -221,6 +251,8 @@ export const articles: Record<ArticleId, ArticleOption> = ids.reduce(
location: paperLocations[id],
enterOptions: paperEnterAnimations[id],
brief: paperBriefs[id],
blocks: contents[id],
initState: states[id],
} satisfies ArticleOption,
};
},

View File

@@ -1,25 +1,26 @@
import { type CSSProperties, useCallback, useState } from 'react';
import { AnimateInTooltip } from './animate-in-tooltip';
import { articles } from './articles';
import { PaperSteps } from './paper-steps';
import * as styles from './style.css';
import type { ArticleId, ArticleOption } from './types';
import type { ArticleId, ArticleOption, OnboardingStatus } from './types';
interface OnboardingProps {
onOpenApp?: () => void;
}
export const Onboarding = (_: OnboardingProps) => {
const [status, setStatus] = useState<{
activeId: ArticleId | null;
unfoldingId: ArticleId | null;
}>({ activeId: null, unfoldingId: null });
export const Onboarding = ({ onOpenApp }: OnboardingProps) => {
const [status, setStatus] = useState<OnboardingStatus>({
activeId: null,
unfoldingId: null,
});
const onFoldChange = useCallback((id: ArticleId, v: boolean) => {
setStatus(s => {
return {
activeId: v ? null : s.activeId,
unfoldingId: v ? null : id,
unfoldingId: v ? s.unfoldingId : id,
};
});
}, []);
@@ -28,7 +29,7 @@ export const Onboarding = (_: OnboardingProps) => {
setStatus(s => {
return {
activeId: v ? null : id,
unfoldingId: s.unfoldingId,
unfoldingId: v ? null : s.unfoldingId,
};
});
}, []);
@@ -62,15 +63,23 @@ export const Onboarding = (_: OnboardingProps) => {
return (
<div style={style} key={id}>
<PaperSteps
status={status}
article={article}
show={status.activeId === null || status.activeId === id}
onFoldChange={onFoldChange}
onFoldChanged={onFoldChanged}
onOpenApp={onOpenApp}
/>
</div>
);
}
)}
<div className={styles.tipsWrapper} data-visible={!status.activeId}>
<AnimateInTooltip
onNext={() => setStatus({ activeId: null, unfoldingId: '4' })}
/>
</div>
</div>
</div>
);

View File

@@ -1,24 +1,30 @@
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { AnimateIn } from './steps/animate-in';
import { EdgelessSwitch } from './steps/edgeless-switch';
import { Unfolding } from './steps/unfolding';
import type { ArticleId, OnboardingStep } from './types';
import type { ArticleId, OnboardingStatus, OnboardingStep } from './types';
import { type ArticleOption } from './types';
interface PaperStepsProps {
show?: boolean;
article: ArticleOption;
status: OnboardingStatus;
onFoldChange?: (id: ArticleId, v: boolean) => void;
onFoldChanged?: (id: ArticleId, v: boolean) => void;
onOpenApp?: () => void;
}
export const PaperSteps = ({
show,
article,
status,
onFoldChange,
onFoldChanged,
onOpenApp,
}: PaperStepsProps) => {
const [stage, setStage] = useState<OnboardingStep>('enter');
const [fold, setFold] = useState(true);
const onEntered = useCallback(() => {
setStage('unfold');
@@ -26,6 +32,7 @@ export const PaperSteps = ({
const _onFoldChange = useCallback(
(v: boolean) => {
setFold(v);
onFoldChange?.(article.id, v);
},
[onFoldChange, article.id]
@@ -34,18 +41,40 @@ export const PaperSteps = ({
const _onFoldChanged = useCallback(
(v: boolean) => {
onFoldChanged?.(article.id, v);
if (!v) setStage('edgeless-switch');
},
[onFoldChanged, article.id]
);
const onEdgelessSwitchBack = useCallback(() => {
setFold(false);
setStage('unfold');
// to apply fold animation
setTimeout(() => _onFoldChange(true));
}, [_onFoldChange]);
useEffect(() => {
if (stage === 'unfold' && status.unfoldingId === article.id) {
console.log('unfold', article.id);
setFold(false);
}
}, [article.id, stage, status.unfoldingId]);
if (!show) return null;
return stage === 'enter' ? (
<AnimateIn article={article} onFinished={onEntered} />
) : stage === 'unfold' ? (
<Unfolding
fold={fold}
article={article}
onChange={_onFoldChange}
onChanged={_onFoldChanged}
/>
) : null;
) : (
<EdgelessSwitch
article={article}
onBack={onEdgelessSwitchBack}
onNext={onOpenApp}
/>
);
};

View File

@@ -13,6 +13,8 @@ interface AnimateInProps {
}
const easing = 'spring(5, 100, 10, 0)';
const segments = 4;
const animeSync = (params: Parameters<typeof anime>[0]) => {
return new Promise(resolve => {
anime({ ...params, complete: () => resolve(null) });
@@ -26,9 +28,8 @@ export const AnimateIn = ({
}: AnimateInProps) => {
const { id: _id, enterOptions, brief } = article;
const id = `onboardingMoveIn${_id}`;
const segments = 4;
const rotateX = enterOptions.curve / segments;
const rotateX = (1.2 * enterOptions.curve) / segments;
useEffect(() => {
Promise.all([
@@ -39,7 +40,7 @@ export const AnimateIn = ({
delay: enterOptions.delay,
}),
animeSync({
targets: `[data-id="${id}"] ${paperStyles.segment}[data-direction="down"]`,
targets: `[data-id="${id}"] .${paperStyles.segment}[data-direction="down"]`,
rotateX: [rotateX, 0],
easing,
delay: enterOptions.delay,

View File

@@ -0,0 +1,118 @@
import { globalStyle, style } from '@vanilla-extract/css';
import { onboardingVars } from '../style.css';
export const edgelessSwitchWindow = style({
vars: { '--bg-size': '10px' },
borderRadius: onboardingVars.paper.r,
backgroundColor: onboardingVars.paper.bg,
position: 'relative',
transition: `width ${onboardingVars.window.transition.size}, height ${onboardingVars.window.transition.size}`,
overflow: 'hidden',
boxShadow: 'var(--affine-shadow-2)',
fontFamily: 'var(--affine-font-family)',
color: onboardingVars.paper.textColor,
selectors: {
'&[data-mode="edgeless"]': {
width: onboardingVars.edgeless.w,
height: onboardingVars.edgeless.h,
},
'&[data-mode="page"]': {
width: onboardingVars.article.w,
height: onboardingVars.article.h,
},
// grid background
'&::before': {
content: '""',
position: 'absolute',
inset: 0,
backgroundImage: onboardingVars.canvas.bgImage,
backgroundRepeat: 'repeat',
backgroundSize: 'calc(24px * var(--scale)) calc(24px * var(--scale))',
backgroundPositionX: 'calc(var(--offset-x) * var(--scale))',
backgroundPositionY: 'calc(var(--offset-y) * var(--scale))',
opacity: 0,
pointerEvents: 'none',
transition: 'opacity 0.3s ease',
},
'&[data-mode="edgeless"]::before': {
opacity: 1,
},
},
});
export const toolbar = style({
position: 'absolute',
bottom: '20px',
left: '50%',
transform: 'translateX(-50%) translateY(200px)',
transition: 'transform 0.3s ease',
selectors: {
[`${edgelessSwitchWindow}[data-mode="edgeless"] &`]: {
transform: 'translateX(-50%) translateY(0px)',
},
},
});
export const canvas = style({
position: 'relative',
width: '100%',
height: '100%',
transform: 'scale(var(--scale)) translate(var(--offset-x), var(--offset-y))',
transition: 'transform 0.36s ease',
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-start',
overflowY: 'visible',
selectors: {
'[data-scroll="true"] &': {
overflowY: 'auto',
},
'[data-mode="edgeless"] &': {
cursor: 'grab',
},
'.grabbing[data-mode="edgeless"] &': {
cursor: 'grabbing',
transition: 'none',
},
'.scaling[data-mode="edgeless"] &': {
transition: 'none',
},
},
});
export const page = style({
width: '800px',
minHeight: onboardingVars.article.h,
paddingTop: '150px',
paddingBottom: '150px',
});
export const noDragWrapper = style({
position: 'absolute',
inset: 0,
pointerEvents: 'none',
});
globalStyle(`${noDragWrapper} > *`, {
pointerEvents: 'auto',
});
export const header = style({
position: 'absolute',
top: '0',
width: '100%',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
padding: '24px',
pointerEvents: 'none',
});
globalStyle(`${header} > *`, {
pointerEvents: 'auto',
});

View File

@@ -0,0 +1,238 @@
import { Button } from '@affine/component';
import { debounce } from 'lodash-es';
import {
type CSSProperties,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { OnboardingBlock } from '../switch-widgets/block';
import { EdgelessSwitchButtons } from '../switch-widgets/switch';
import { ToolbarSVG } from '../switch-widgets/toolbar';
import type {
ArticleOption,
EdgelessSwitchMode,
EdgelessSwitchState,
} from '../types';
import * as styles from './edgeless-switch.css';
interface EdgelessSwitchProps {
article: ArticleOption;
onBack?: () => void;
onNext?: () => void;
}
const offsetXRanges = [-2000, 2000];
const offsetYRanges = [-2000, 2000];
const scaleRange = [0.2, 2];
const defaultState: EdgelessSwitchState = {
scale: 0.5,
offsetX: 0,
offsetY: 0,
};
export const EdgelessSwitch = ({
article,
onBack,
onNext,
}: EdgelessSwitchProps) => {
const windowRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLDivElement>(null);
const mouseDownRef = useRef(false);
const prevStateRef = useRef<EdgelessSwitchState | null>(
article.initState ?? null
);
const enableScrollTimerRef = useRef<ReturnType<typeof setTimeout>>();
const turnOffScalingRef = useRef<() => void>(() => {});
const [scrollable, setScrollable] = useState(false);
const [mode, setMode] = useState<EdgelessSwitchMode>('page');
const [state, setState] = useState<EdgelessSwitchState>({
scale: 1,
offsetX: 0,
offsetY: 0,
});
const onSwitchToPageMode = useCallback(() => setMode('page'), []);
const onSwitchToEdgelessMode = useCallback(() => setMode('edgeless'), []);
const toggleGrabbing = useCallback((v: boolean) => {
if (!windowRef.current) return;
windowRef.current.classList.toggle('grabbing', v);
}, []);
const turnOnScaling = useCallback(() => {
if (!windowRef.current) return;
windowRef.current.classList.add('scaling');
}, []);
const enableScrollWithDelay = useCallback(() => {
return new Promise<any>(resolve => {
enableScrollTimerRef.current = setTimeout(() => {
setScrollable(true);
resolve(true);
}, 500);
});
}, []);
const disableScroll = useCallback(() => {
if (enableScrollTimerRef.current)
clearTimeout(enableScrollTimerRef.current);
setScrollable(false);
}, []);
const setStateAndSave = useCallback((state: EdgelessSwitchState) => {
setState(state);
prevStateRef.current = state;
}, []);
const onNextClick = useCallback(() => {
if (mode === 'page') setMode('edgeless');
else onNext?.();
}, [mode, onNext]);
useEffect(() => {
turnOffScalingRef.current = debounce(() => {
if (!windowRef.current) return;
windowRef.current.classList.remove('scaling');
}, 100);
}, []);
useEffect(() => {
if (mode === 'page') return;
const canvas = canvasRef.current;
const win = windowRef.current;
if (!win || !canvas) return;
const onWheel = (e: WheelEvent) => {
turnOnScaling();
const { deltaY } = e;
const newScale = state.scale - deltaY * 0.001;
const safeScale = Math.max(
Math.min(newScale, scaleRange[1]),
scaleRange[0]
);
setStateAndSave({ ...state, scale: safeScale });
turnOffScalingRef.current?.();
};
// TODO: mobile support
const onMouseDown = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (target.closest('[data-no-drag]')) return;
e.preventDefault();
mouseDownRef.current = true;
toggleGrabbing(true);
};
const onMouseMove = (e: MouseEvent) => {
if (!mouseDownRef.current) return;
const offsetX = state.offsetX + e.movementX / state.scale;
const offsetY = state.offsetY + e.movementY / state.scale;
const safeOffsetX = Math.max(
Math.min(offsetX, offsetXRanges[1]),
offsetXRanges[0]
);
const safeOffsetY = Math.max(
Math.min(offsetY, offsetYRanges[1]),
offsetYRanges[0]
);
setStateAndSave({
scale: state.scale,
offsetX: safeOffsetX,
offsetY: safeOffsetY,
});
};
const onMouseUp = (_: MouseEvent) => {
mouseDownRef.current = false;
toggleGrabbing(false);
};
win.addEventListener('wheel', onWheel);
win.addEventListener('mousedown', onMouseDown);
window.addEventListener('mouseup', onMouseUp);
window.addEventListener('mousemove', onMouseMove);
return () => {
win.removeEventListener('wheel', onWheel);
win.removeEventListener('mousedown', onMouseDown);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
}, [
mode,
state,
state.offsetX,
state.offsetY,
state.scale,
setStateAndSave,
toggleGrabbing,
turnOnScaling,
]);
// to avoid `overflow: auto` clip the content before animation ends
useEffect(() => {
if (mode === 'page') {
enableScrollWithDelay()
.then(() => {
// handle scroll
canvasRef.current?.scrollTo({ top: 0 });
})
.catch(console.error);
setState({ scale: 1, offsetX: 0, offsetY: 0 });
} else {
disableScroll();
canvasRef.current?.scrollTo({ top: 0 });
// save state when switching between modes
setState(prevStateRef.current ?? defaultState);
}
}, [disableScroll, enableScrollWithDelay, mode]);
const canvasStyle = {
'--scale': state.scale,
'--offset-x': state.offsetX + 'px',
'--offset-y': state.offsetY + 'px',
} as CSSProperties;
return (
<div
ref={windowRef}
data-mode={mode}
data-scroll={scrollable}
className={styles.edgelessSwitchWindow}
style={canvasStyle}
>
<div className={styles.canvas} ref={canvasRef}>
<div className={styles.page}>
{
/* render blocks */
article.blocks.map((block, key) => {
return <OnboardingBlock key={key} mode={mode} {...block} />;
})
}
</div>
</div>
<div data-no-drag className={styles.noDragWrapper}>
<header className={styles.header}>
<Button size="extraLarge" onClick={onBack}>
Back
</Button>
<EdgelessSwitchButtons
mode={mode}
onSwitchToPageMode={onSwitchToPageMode}
onSwitchToEdgelessMode={onSwitchToEdgelessMode}
/>
<Button size="extraLarge" type="primary" onClick={onNextClick}>
Next
</Button>
</header>
<div className={styles.toolbar}>
<ToolbarSVG />
</div>
</div>
</div>
);
};

View File

@@ -56,8 +56,8 @@ export const unfoldingWrapper = style([
},
width: onboardingVars.article.w,
height: onboardingVars.article.h,
left: `calc(0 - ${onboardingVars.article.w} / 2)`,
top: `calc(0 - ${onboardingVars.article.h} / 2)`,
left: `calc(-${onboardingVars.article.w} / 2)`,
top: `calc(-${onboardingVars.article.h} / 2)`,
},
},
},

View File

@@ -1,47 +1,53 @@
import clsx from 'clsx';
import { useCallback, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { ArticleOption } from '../types';
import * as styles from './unfolding.css';
interface UnfoldingProps {
fold: boolean;
article: ArticleOption;
initialFold?: boolean;
onChange?: (e: boolean) => void;
onChanged?: (e: boolean) => void;
article: ArticleOption;
}
export const Unfolding = ({ article, onChange, onChanged }: UnfoldingProps) => {
const [fold, setFold] = useState(true);
export const Unfolding = ({
fold,
article,
onChange,
onChanged,
}: UnfoldingProps) => {
const [folding, setFolding] = useState(fold);
const ref = useRef<HTMLDivElement>(null);
const toggleFold = useCallback(() => {
setFold(!fold);
return !fold;
}, [fold]);
onChange?.(!fold);
}, [fold, onChange]);
const onPaperClick = useCallback(() => {
const isFold = toggleFold();
onChange?.(isFold);
useEffect(() => {
setFolding(fold);
const paper = ref.current;
if (ref.current) {
if (paper) {
const handler = () => {
onChanged?.(isFold);
onChanged?.(fold);
};
ref.current.addEventListener('transitionend', handler, { once: true });
return () => ref.current?.removeEventListener('transitionend', handler);
return () => paper?.removeEventListener('transitionend', handler);
}
return null;
}, [toggleFold, onChange, onChanged]);
return () => null;
}, [fold, onChanged]);
return (
<div
ref={ref}
data-fold={fold}
data-fold={folding}
className={styles.unfoldingWrapper}
onClick={onPaperClick}
onClick={toggleFold}
>
<div className={clsx(styles.unfoldingContent, !fold && 'leave')}>
<div className={clsx(styles.unfoldingContent, !folding && 'leave')}>
{article.brief}
</div>
</div>

View File

@@ -3,7 +3,7 @@ import { globalStyle, style } from '@vanilla-extract/css';
// in case that we need to support dark mode later
export const onboardingVars = {
window: {
bg: 'var(--affine-pure-white)',
bg: 'white',
shadow: 'var(--affine-shadow-1)',
transition: {
size: '0.3s ease',
@@ -13,7 +13,7 @@ export const onboardingVars = {
w: '230px',
h: '302px',
r: '8px',
bg: 'var(--affine-pure-white)',
bg: 'white',
// textColor: 'var(--affine-light-text-primary-color)',
textColor: '#121212',
borderColor: '#E3E2E4',
@@ -23,7 +23,7 @@ export const onboardingVars = {
transformTransition: '0.3s ease',
},
web: {
bg: '#fafafa', // TODO: use var
bg: '#fafafa',
},
article: {
@@ -34,6 +34,27 @@ export const onboardingVars = {
w: '1200px',
h: '800px',
},
canvas: {
width: 3500,
height: 3500,
pageBlockWidth: 800,
bgImage: 'radial-gradient(#e6e6e6 1px, #fff 1px)',
},
toolbar: {
bg: 'white',
borderColor: '#E3E2E4',
},
block: {
transition: '0.5s ease',
},
animateIn: {
tooltipShowUpDelay: '5s',
nextButtonShowUpDelay: '20s',
},
};
export const perspective = style({
@@ -86,3 +107,20 @@ export const paperLocation = style({
left: `calc(var(--offset-x) - ${onboardingVars.paper.w} / 2)`,
top: `calc(var(--offset-y) - ${onboardingVars.paper.h} / 2)`,
});
export const tipsWrapper = style({
position: 'absolute',
width: `calc(${onboardingVars.article.w} - 48px)`,
top: `calc(-${onboardingVars.article.h} / 2 + 24px)`,
pointerEvents: 'none',
display: 'flex',
justifyContent: 'center',
opacity: 0,
transition: '0.3s ease 1s',
selectors: {
'&[data-visible="true"]': {
pointerEvents: 'auto',
opacity: 1,
},
},
});

View File

@@ -0,0 +1,68 @@
import type { CSSProperties } from 'react';
import { type EdgelessSwitchMode, type OnboardingBlockOption } from '../types';
import { onboardingBlock } from './style.css';
interface OnboardingBlockProps extends OnboardingBlockOption {
mode: EdgelessSwitchMode;
}
export const OnboardingBlock = ({
bg,
mode,
style,
children,
offset,
position,
fromPosition,
enterDelay,
leaveDelay,
edgelessOnly,
customStyle,
sub,
}: OnboardingBlockProps) => {
const baseStyles = {
'--bg': bg,
'--enter-delay': enterDelay ? `${enterDelay}ms` : '0ms',
'--leave-delay': leaveDelay ? `${leaveDelay}ms` : '0ms',
zIndex: position ? 1 : 0,
position: position || fromPosition ? 'absolute' : 'relative',
} as CSSProperties;
if (mode === 'page') {
if (fromPosition) {
baseStyles.left = fromPosition.x ?? 'unset';
baseStyles.top = fromPosition.y ?? 'unset';
}
} else {
if (offset) {
baseStyles.transform = `translate(${offset.x}px, ${offset.y}px)`;
}
if (position) {
baseStyles.left = position.x ?? 'unset';
baseStyles.top = position.y ?? 'unset';
}
}
const blockStyles = {
...baseStyles,
...style,
...customStyle?.[mode],
} as CSSProperties;
return (
<div
style={blockStyles}
onMouseDown={e => {
e.stopPropagation();
}}
className={onboardingBlock}
data-mode={mode}
data-bg-mode={bg && mode === 'edgeless'}
data-invisible={mode === 'page' && edgelessOnly}
>
{children}
{sub ? <OnboardingBlock mode={mode} {...sub} /> : null}
</div>
);
};

View File

@@ -0,0 +1,49 @@
import { keyframes, style } from '@vanilla-extract/css';
export const counterNote = style({
display: 'flex',
alignItems: 'center',
gap: '8px',
});
export const count = style({
width: '50px',
height: '50px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
});
export const label = style({
fontSize: '16px',
fontFamily: '"Orelega One"',
fontWeight: 700,
lineHeight: '18px',
width: 0,
flexGrow: 1,
});
const drawCircleLine = keyframes({
from: { strokeDashoffset: 120 },
to: { strokeDashoffset: 0 },
});
export const circleAnim = style({
strokeDashoffset: 120,
selectors: {
'[data-mode="edgeless"] &': {
animation: `${drawCircleLine} 0.5s ease forwards`,
},
},
});
const numberIn = keyframes({
from: { opacity: 0 },
to: { opacity: 1 },
});
export const fadeInAnim = style({
opacity: 0,
selectors: {
'[data-mode="edgeless"] &': {
animation: `${numberIn} 0.2s ease forwards`,
},
},
});

View File

@@ -0,0 +1,64 @@
import clsx from 'clsx';
import * as styles from './counter-note.css';
interface CounterNoteProps {
index: 1 | 2 | 3 | 4 | 5;
label: string;
width: number;
color?: string;
animationDelay?: number;
}
const D_MAP = {
1: 'M20.6505 10.9611C20.6505 11.9045 20.5158 12.9677 20.2462 14.1507C19.9767 15.3187 19.7221 16.3594 19.4825 17.2728C19.2429 18.1862 19.0258 19.0697 18.8311 19.9232C18.6515 20.7768 18.4942 21.4731 18.3595 22.0121C18.2397 22.5362 18.0974 23.0753 17.9327 23.6294C17.6033 24.6925 17.2439 25.2241 16.8545 25.2241C16.5101 25.2241 16.1133 25.007 15.6641 24.5727C15.2298 24.1235 15.0127 23.7192 15.0127 23.3598C15.0127 23.0004 15.1774 22.1769 15.5069 20.8891C15.8363 19.6013 16.1657 17.9167 16.4952 15.8353C16.8396 13.7389 17.0118 11.5601 17.0118 9.29901C17.0118 8.86475 17.2289 8.64763 17.6632 8.64763C18.1124 8.64763 18.7188 8.96208 19.4825 9.591C20.2612 10.2199 20.6505 10.6766 20.6505 10.9611Z',
2: 'M14.7984 22.0043C17.4788 21.5102 19.5078 21.2631 20.8854 21.2631C22.2781 21.2631 22.9744 21.5626 22.9744 22.1616C22.9744 22.7306 21.7839 23.3445 19.403 24.0034C17.0371 24.6623 15.2177 24.9917 13.9449 24.9917C13.4208 24.9917 12.7844 24.6548 12.0357 23.9809C11.287 23.3071 10.9126 22.7081 10.9126 22.184C10.9126 21.8396 11.871 20.8813 13.7877 19.309C14.5813 18.6501 15.3749 17.9613 16.1686 17.2425C16.9622 16.5088 17.636 15.7601 18.1901 14.9964C18.7591 14.2327 19.0436 13.5514 19.0436 12.9524C19.0436 12.3385 18.7591 12.0315 18.1901 12.0315C17.7558 12.0315 17.3665 12.1962 17.0221 12.5256C16.6927 12.8401 16.4306 13.162 16.2359 13.4915C16.0563 13.8059 15.9215 13.9632 15.8316 13.9632C15.4423 13.9632 14.9482 13.7535 14.3492 13.3342C13.7652 12.9 13.4732 12.5256 13.4732 12.2112C13.4732 11.3576 13.9224 10.549 14.8209 9.78535C15.7343 9.00669 16.8274 8.61735 18.1002 8.61735C19.388 8.61735 20.3988 9.01417 21.1325 9.80781C21.8663 10.5865 22.2331 11.5823 22.2331 12.7952C22.2331 14.4274 21.5293 16.067 20.1218 17.7142C18.7292 19.3614 16.9547 20.7914 14.7984 22.0043Z',
3: 'M14.1695 17.1104V17.0206C14.1695 16.8409 14.2519 16.6912 14.4166 16.5714C14.6112 16.3318 14.9931 16.0023 15.5621 15.5831C16.1311 15.1638 16.6627 14.7745 17.1569 14.4151C17.666 14.0407 18.1227 13.599 18.527 13.0899C18.9313 12.5807 19.1335 12.0866 19.1335 11.6074C19.1335 11.4277 19.0661 11.3379 18.9313 11.3379C18.4372 11.3379 17.7334 11.5849 16.8199 12.0791C15.9065 12.5583 15.3225 12.7979 15.068 12.7979C14.8134 12.7979 14.4091 12.5358 13.855 12.0117C13.301 11.4726 13.024 11.0758 13.024 10.8213C13.024 10.402 13.3759 9.99768 14.0797 9.60835C14.7834 9.20404 15.5846 8.89707 16.483 8.68743C17.3964 8.46282 18.1976 8.35051 18.8864 8.35051C19.8747 8.35051 20.7282 8.70989 21.447 9.42866C22.1807 10.1474 22.5476 10.9036 22.5476 11.6973C22.5476 12.4909 22.2107 13.3145 21.5368 14.168C20.878 15.0066 20.0544 15.7628 19.0661 16.4366C21.1625 16.9008 22.2107 17.8517 22.2107 19.2892C22.2107 20.2026 21.6791 21.1011 20.6159 21.9846C19.5677 22.8531 18.2949 23.5419 16.7975 24.051C15.3001 24.5601 13.885 24.8147 12.5523 24.8147C10.9351 24.8147 10.1265 24.4628 10.1265 23.759C10.1265 23.3697 11.4741 22.7782 14.1695 21.9846C15.1728 21.7 16.0937 21.3107 16.9322 20.8166C17.7858 20.3224 18.2125 19.8133 18.2125 19.2892C18.2125 19.0047 18.0628 18.7876 17.7633 18.6378C17.4638 18.4731 17.1269 18.3683 16.7526 18.3234C15.6145 18.1886 14.8733 17.9864 14.5289 17.7169C14.2893 17.5672 14.1695 17.365 14.1695 17.1104Z',
4: 'M20.1218 12.0789C20.4961 12.0789 20.9978 12.3559 21.6267 12.9099C22.2706 13.464 22.5925 13.9207 22.5925 14.2801C22.5925 15.1486 22.0609 17.2375 20.9978 20.5468C19.9346 23.8411 19.1335 25.4883 18.5944 25.4883C18.3099 25.4883 17.9879 25.2786 17.6285 24.8594C17.2841 24.4251 17.1119 24.0582 17.1119 23.7588C17.1119 23.4593 17.1494 23.1448 17.2242 22.8154C17.2991 22.471 17.374 22.1565 17.4489 21.872C17.5387 21.5725 17.666 21.1532 17.8307 20.6142C18.0104 20.0751 18.1526 19.6408 18.2575 19.3114C17.1793 19.6858 16.1162 19.8729 15.068 19.8729C14.0347 19.8729 13.2261 19.5884 12.6421 19.0194C12.0731 18.4504 11.7886 17.6867 11.7886 16.7284C11.7886 16.0395 12.1555 14.9913 12.8892 13.5838C13.6379 12.1762 14.4615 10.9109 15.36 9.78779C16.2584 8.66472 16.8948 8.10319 17.2692 8.10319C17.6585 8.10319 18.0553 8.29037 18.4596 8.66473C18.8789 9.03908 19.0885 9.39098 19.0885 9.72041C19.0885 10.0349 18.3847 11.1879 16.9772 13.1795C15.5846 15.171 14.8883 16.3465 14.8883 16.7059C14.8883 16.8556 14.9182 16.953 14.9781 16.9979C15.038 17.0428 15.1728 17.0653 15.3824 17.0653C15.8915 17.0653 16.6253 16.8631 17.5836 16.4588C18.542 16.0545 19.0586 15.8449 19.1335 15.8299C19.3281 14.7667 19.4255 13.741 19.4255 12.7527C19.4255 12.3035 19.6576 12.0789 20.1218 12.0789Z',
5: 'M19.5602 8.48552C22.3005 8.48552 23.6707 8.81496 23.6707 9.47382C23.6707 9.75833 23.4386 10.0054 22.9744 10.215C22.4203 10.4846 21.7465 10.7092 20.9528 10.8889C20.1742 11.0686 19.5453 11.2108 19.0661 11.3157C18.6019 11.4205 18.235 11.5103 17.9655 11.5852C17.6959 11.6451 17.4264 11.9521 17.1569 12.5061C16.9023 13.0602 16.775 13.4944 16.775 13.8089C16.775 14.1233 16.9173 14.4528 17.2018 14.7972C17.5013 15.1416 17.8607 15.4785 18.2799 15.8079C18.6992 16.1374 19.1185 16.4818 19.5378 16.8412C20.541 17.6947 21.0427 18.5632 21.0427 19.4467C21.0427 20.8093 20.3688 22.0597 19.0212 23.1977C17.6884 24.3208 16.3782 24.8823 15.0904 24.8823C13.8026 24.8823 12.702 24.5379 11.7886 23.8491C10.8901 23.1453 10.4409 22.3517 10.4409 21.4682C10.4409 21.1837 10.4933 20.9366 10.5981 20.727C10.703 20.5173 10.8452 20.4125 11.0249 20.4125C11.2046 20.4125 11.4367 20.5473 11.7212 20.8168C12.6047 21.6554 13.3908 22.0746 14.0797 22.0746C14.7685 22.0746 15.5097 21.7901 16.3033 21.2211C17.1119 20.6521 17.5162 20.0831 17.5162 19.5141C17.5162 19.0049 17.0146 18.4134 16.0113 17.7396C15.5921 17.4551 15.1728 17.1481 14.7535 16.8187C14.3492 16.4893 13.9973 16.07 13.6978 15.5609C13.4133 15.0517 13.271 14.4378 13.271 13.719C13.271 13.0003 13.6304 12.0045 14.3492 10.7317C15.068 9.44387 15.7268 8.74757 16.3258 8.64275C16.9248 8.53793 18.0029 8.48552 19.5602 8.48552Z',
};
export const CounterNote = ({
index,
label,
width,
color = 'currentColor',
animationDelay = 200,
}: CounterNoteProps) => {
return (
<div style={{ width }} className={styles.counterNote}>
<div className={styles.count}>
<svg
width="35"
height="37"
viewBox="0 0 35 37"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
style={{ animationDelay: `${animationDelay}ms` }}
className={styles.fadeInAnim}
d={D_MAP[index] ?? ''}
fill={color}
/>
<path
style={{ animationDelay: `${animationDelay + 200}ms` }}
className={styles.circleAnim}
d="M21.0208 1.61363C17.955 1.61363 14.645 1.19536 11.7113 2.19547C9.48766 2.95353 7.67386 4.69528 6.11662 6.39627C4.27176 8.41142 2.88097 11.0381 2.31225 13.7237C2.03 15.0565 2.03092 16.435 2.03092 17.7902C2.03092 19.3224 1.93456 20.8926 2.08207 22.4194C2.46462 26.3788 5.22971 29.7171 8.38646 31.9079C10.6706 33.4932 13.3724 34.3043 16.0975 34.6957C18.2128 34.9995 20.6588 35.4449 22.7472 34.7276C24.0477 34.281 25.4641 33.2759 26.5196 32.4067C27.7642 31.3816 28.8421 30.0236 29.6526 28.6343C31.2273 25.9348 32.0282 22.577 32.5043 19.5166C32.9289 16.7864 32.9403 13.9927 32.8687 11.2364C32.8081 8.90408 32.3903 5.74734 30.4326 4.14561C28.1828 2.30487 25.5133 2.76453 22.8623 2.76453"
stroke={color}
strokeWidth="2.99485"
strokeLinecap="round"
strokeDasharray="120 120"
/>
</svg>
</div>
<div
className={clsx(styles.label, styles.fadeInAnim)}
style={{ color, animationDelay: `${animationDelay + 700}ms` }}
>
{label}
</div>
</div>
);
};

View File

@@ -0,0 +1,15 @@
import { LinkedPageIcon } from '@blocksuite/icons';
import type { PropsWithChildren } from 'react';
import { pageLink, pageLinkIcon, pageLinkLabel } from '../articles/blocks.css';
interface PageLinkProps extends PropsWithChildren {}
export const PageLink = ({ children }: PageLinkProps) => {
return (
<a className={pageLink}>
<LinkedPageIcon className={pageLinkIcon} />
<span className={pageLinkLabel}>{children}</span>
</a>
);
};

View File

@@ -0,0 +1,29 @@
import type { PropsWithChildren } from 'react';
import { shadowSticker } from './style.css';
interface ShadowStickerProps extends PropsWithChildren {
color?: string;
width?: number;
animate?: boolean;
}
export const ShadowSticker = ({
color = '#F9E8FF',
width,
animate = true,
children,
}: ShadowStickerProps) => {
return (
<div
data-animate={animate}
className={shadowSticker}
style={{
backgroundColor: color,
width,
}}
>
{children}
</div>
);
};

View File

@@ -0,0 +1,149 @@
import { keyframes, style } from '@vanilla-extract/css';
import { block } from '../articles/blocks.css';
import { onboardingVars } from '../style.css';
export const switchButtons = style({
display: 'flex',
alignItems: 'center',
gap: '16px',
padding: '8px',
borderRadius: '24px',
background: '#F4F4F5',
height: '64px',
width: '128px',
justifyContent: 'space-between',
selectors: {
// indicator
'&::after': {
content: '',
width: '48px',
height: '48px',
borderRadius: '16px',
backgroundColor: 'white',
position: 'absolute',
transition: 'transform 0.15s ease',
boxShadow: 'var(--affine-shadow-1)',
},
'&[data-mode="edgeless"]::after': {
transform: `translateX(64px)`,
},
},
});
export const switchButton = style({
transform: 'scale(2)',
boxShadow: 'none',
opacity: 0.6,
selectors: {
'&:nth-child(1)': {
transformOrigin: 'left',
},
'&:nth-child(2)': {
transformOrigin: 'right',
},
'&[data-active="true"]': {
opacity: 1,
},
},
});
const pop = keyframes({
from: { transform: 'translateY(100%)' },
to: { transform: 'translateY(0)' },
});
export const toolbar = style({
cursor: 'not-allowed',
boxShadow: '0px 0px 12px 0px #4241492E',
borderRadius: '16px',
border: `1px solid ${onboardingVars.toolbar.borderColor}`,
height: '65px',
overflow: 'hidden',
backgroundColor: onboardingVars.toolbar.bg,
});
export const toolbarPop = style({
vars: {
'--delay': '0s',
},
selectors: {
'[data-mode="edgeless"] &': {
transform: 'translateY(120%)',
animation: `${pop} 0.4s cubic-bezier(.04,1.01,.42,1.31) forwards`,
animationDelay: 'var(--delay)',
},
},
});
export const onboardingBlock = style([
block,
{
vars: {
'--enter-delay': '0ms',
'--leave-delay': '0ms',
},
padding: '0 28px',
cursor: 'unset',
borderRadius: '8px',
display: 'flex',
flexDirection: 'column',
gap: '16px',
marginBottom: '16px',
border: '2px solid transparent',
selectors: {
'&[data-bg-mode="true"]': {
background: 'var(--bg)',
// boxShadow: 'var(--affine-menu-shadow)', // dark-mode issue
boxShadow:
'0px 0px 12px 0px rgba(66, 65, 73, 0.14), 0px 0px 0px 0.5px #E3E3E4 inset',
padding: '18px 28px',
borderColor: 'rgba(0, 0, 0, 0.1)',
},
'&[data-invisible="true"]': {
opacity: 0,
pointerEvents: 'none',
marginBottom: 0,
},
'&:last-child': {
marginBottom: 0,
},
'&[data-mode="edgeless"]': {
transition: `all ${onboardingVars.block.transition} var(--enter-delay)`,
},
'&[data-mode="page"]': {
transition: `all ${onboardingVars.block.transition} var(--leave-delay)`,
},
},
},
]);
export const shadowSticker = style({
position: 'relative',
borderRadius: '8px',
boxShadow: '10px 10px 0px 6px #000',
padding: '22px 24px',
fontSize: '15px',
lineHeight: '23px',
selectors: {
'&::before': {
content: '""',
position: 'absolute',
inset: 0,
pointerEvents: 'none',
borderRadius: 'inherit',
boxShadow: '0px 0px 0px 6px #000',
},
// use data-mode to apply animation only for edgeless mode
// this is a hacky way to do it
'[data-mode=edgeless] &[data-animate=true]': {
animation: `${keyframes({
from: { boxShadow: '0px 0px 0px 0px #000' },
to: { boxShadow: '10px 10px 0px 6px #000' },
})} 0.6s ease forwards`,
},
},
});

View File

@@ -0,0 +1,44 @@
import clsx from 'clsx';
import type { HTMLAttributes } from 'react';
import {
EdgelessSwitchItem,
PageSwitchItem,
} from '../../../blocksuite/block-suite-mode-switch/switch-items';
import type { EdgelessSwitchMode } from '../types';
import * as styles from './style.css';
interface EdgelessSwitchProps extends HTMLAttributes<HTMLDivElement> {
mode: EdgelessSwitchMode;
onSwitchToPageMode: () => void;
onSwitchToEdgelessMode: () => void;
}
export const EdgelessSwitchButtons = ({
mode,
className,
onSwitchToPageMode,
onSwitchToEdgelessMode,
...attrs
}: EdgelessSwitchProps) => {
return (
<div
data-mode={mode}
className={clsx(styles.switchButtons, className)}
{...attrs}
>
<PageSwitchItem
className={styles.switchButton}
data-active={mode === 'page'}
active={mode === 'page'}
onClick={onSwitchToPageMode}
/>
<EdgelessSwitchItem
className={styles.switchButton}
data-active={mode === 'edgeless'}
active={mode === 'edgeless'}
onClick={onSwitchToEdgelessMode}
/>
</div>
);
};

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,9 @@
import type { ReactNode } from 'react';
import type { CSSProperties } from '@vanilla-extract/css';
import type { PropsWithChildren, ReactNode } from 'react';
export type OnboardingStep = 'enter' | 'unfold' | 'mode-switch';
export type OnboardingStep = 'enter' | 'unfold' | 'edgeless-switch';
export type ArticleId = '0' | '1' | '2' | '3' | '4';
export type EdgelessSwitchMode = 'edgeless' | 'page';
/**
* Paper enter animation options
@@ -44,4 +46,53 @@ export interface ArticleOption {
/** offset Y */
y: number;
};
/** content that contains edgeless info */
blocks: OnboardingBlockOption[];
/** apply an initial state for edgeless mode */
initState?: EdgelessSwitchState;
}
export interface OnboardingBlockOption extends PropsWithChildren {
/**
* if set, will apply special background styled for edgeless mode
*/
bg?: string;
/**
* only show in edgeless mode
*/
edgelessOnly?: boolean;
/** apply transform */
offset?: { x?: number; y?: number };
/** apply absolute position for edgeless mode */
position?: { x?: number; y?: number };
/** apply absolute position for page mode */
fromPosition?: { x?: number; y?: number };
/** enter delay in ms */
enterDelay?: number;
/** leave delay in ms */
leaveDelay?: number;
style?: CSSProperties;
/** customize style for different mode */
customStyle?: {
page?: CSSProperties;
edgeless?: CSSProperties;
};
/** attach a sub block to current block */
sub?: OnboardingBlockOption;
}
export interface EdgelessSwitchState {
scale: number;
offsetX: number;
offsetY: number;
}
export interface OnboardingStatus {
activeId: ArticleId | null;
unfoldingId: ArticleId | null;
}

View File

@@ -13,3 +13,13 @@ declare module '*.assets.svg' {
const url: string;
export default url;
}
declare module '*.png' {
const url: string;
export default url;
}
declare module '*.jpg' {
const url: string;
export default url;
}