mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(client/curriculum): add dialogue animations (#52543)
This commit is contained in:
@@ -123,6 +123,41 @@ exports.createPages = function createPages({ graphql, actions, reporter }) {
|
||||
superOrder
|
||||
template
|
||||
usesMultifileEditor
|
||||
scene {
|
||||
setup {
|
||||
background
|
||||
characters {
|
||||
character
|
||||
position {
|
||||
x
|
||||
y
|
||||
z
|
||||
}
|
||||
}
|
||||
audio {
|
||||
filename
|
||||
startTime
|
||||
startTimestamp
|
||||
finishTimestamp
|
||||
}
|
||||
alwaysShowDialogue
|
||||
}
|
||||
commands {
|
||||
background
|
||||
character
|
||||
position {
|
||||
x
|
||||
y
|
||||
z
|
||||
}
|
||||
startTime
|
||||
finishTime
|
||||
dialogue {
|
||||
text
|
||||
align
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,6 +338,7 @@ exports.createSchemaCustomization = ({ actions }) => {
|
||||
prerequisites: [PrerequisiteChallenge]
|
||||
msTrophyId: String
|
||||
fillInTheBlank: FillInTheBlank
|
||||
scene: Scene
|
||||
}
|
||||
type FileContents {
|
||||
fileKey: String
|
||||
@@ -325,6 +361,45 @@ exports.createSchemaCustomization = ({ actions }) => {
|
||||
answer: String
|
||||
feedback: String
|
||||
}
|
||||
type Scene {
|
||||
setup: SceneSetup
|
||||
commands: [SceneCommands]
|
||||
}
|
||||
type SceneSetup {
|
||||
background: String
|
||||
characters: [SetupCharacter]
|
||||
audio: SetupAudio
|
||||
alwaysShowDialogue: Boolean
|
||||
}
|
||||
type SetupCharacter {
|
||||
character: String
|
||||
position: CharacterPosition
|
||||
opacity: Float
|
||||
}
|
||||
type SetupAudio {
|
||||
filename: String
|
||||
startTime: Float
|
||||
startTimestamp: Float
|
||||
finishTimestamp: Float
|
||||
}
|
||||
type SceneCommands {
|
||||
background: String
|
||||
character: String
|
||||
position: CharacterPosition
|
||||
opacity: Float
|
||||
startTime: Float
|
||||
finishTime: Float
|
||||
dialogue: Dialogue
|
||||
}
|
||||
type Dialogue {
|
||||
text: String
|
||||
align: String
|
||||
}
|
||||
type CharacterPosition {
|
||||
x: Float
|
||||
y: Float
|
||||
z: Float
|
||||
}
|
||||
`;
|
||||
createTypes(typeDefs);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
|
||||
function AccessibilityIcon(
|
||||
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>
|
||||
): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
fill={props.fill || 'var(--gray-00)'}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='452.000000pt'
|
||||
height='452.000000pt'
|
||||
viewBox='0 0 452.000000 452.000000'
|
||||
preserveAspectRatio='xMidYMid meet'
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
transform='translate(0.000000,452.000000) scale(0.100000,-0.100000)'
|
||||
fill={props.fill || 'var(--gray-00)'}
|
||||
stroke='none'
|
||||
>
|
||||
<path
|
||||
d='M1980 4325 c-683 -95 -1267 -511 -1578 -1123 -153 -301 -222 -593
|
||||
-222 -942 0 -349 69 -641 222 -942 209 -413 540 -735 963 -939 279 -135 564
|
||||
-198 892 -199 348 0 645 69 945 222 625 317 1043 917 1128 1618 15 124 12 382
|
||||
-5 517 -120 926 -862 1668 -1788 1788 -143 18 -424 18 -557 0z m539 -190 c854
|
||||
-122 1514 -795 1621 -1655 13 -109 13 -331 0 -440 -60 -485 -295 -919 -665
|
||||
-1228 -347 -291 -763 -442 -1215 -442 -306 0 -588 67 -854 203 -838 428 -1237
|
||||
1397 -941 2285 65 195 187 421 312 577 299 376 733 626 1213 699 126 19 398
|
||||
19 529 1z'
|
||||
/>
|
||||
<path
|
||||
d='M2153 3876 c-218 -71 -320 -310 -221 -515 31 -63 107 -137 176 -169
|
||||
49 -23 70 -27 152 -27 82 0 103 4 152 27 278 130 285 525 12 659 -55 27 -80
|
||||
33 -147 36 -51 2 -98 -2 -124 -11z m183 -195 c97 -44 128 -171 63 -263 -24
|
||||
-33 -95 -68 -139 -68 -44 0 -115 35 -139 68 -65 92 -34 219 63 263 23 10 57
|
||||
19 76 19 19 0 53 -9 76 -19z'
|
||||
/>
|
||||
<path
|
||||
d='M1075 3196 c-93 -44 -135 -112 -135 -218 1 -76 28 -137 82 -181 26
|
||||
-21 143 -67 405 -161 l368 -131 3 -302 2 -302 -115 -460 c-63 -254 -115 -475
|
||||
-115 -492 0 -120 104 -228 220 -229 75 0 121 18 171 69 46 46 47 50 170 418
|
||||
68 205 126 373 129 373 3 0 61 -168 129 -372 123 -369 124 -373 170 -419 50
|
||||
-51 96 -69 171 -69 116 1 220 109 220 229 0 17 -52 238 -115 492 l-115 460 2
|
||||
301 3 302 368 131 c263 95 378 141 404 162 56 45 83 104 83 180 0 107 -42 175
|
||||
-135 219 -87 41 -113 36 -489 -91 l-338 -115 -358 0 -357 0 -339 115 c-376
|
||||
127 -402 132 -489 91z m451 -281 l352 -115 382 0 382 0 352 115 c193 63 354
|
||||
115 358 115 4 0 14 -7 22 -16 17 -17 20 -42 8 -62 -5 -6 -197 -79 -428 -161
|
||||
l-419 -149 -3 -384 -3 -383 117 -467 c95 -382 114 -471 104 -483 -16 -19 -44
|
||||
-19 -59 -1 -15 17 -331 963 -331 989 0 36 -29 75 -65 87 -66 22 -135 -23 -135
|
||||
-87 0 -26 -316 -972 -331 -989 -15 -18 -43 -18 -59 1 -10 12 9 101 104 483
|
||||
l117 467 -3 384 -3 383 -420 149 c-230 82 -423 155 -427 162 -12 19 -9 44 8
|
||||
61 8 9 18 16 22 16 4 0 165 -52 358 -115z'
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
AccessibilityIcon.displayName = 'AccessibilityIcon';
|
||||
|
||||
export default AccessibilityIcon;
|
||||
@@ -87,6 +87,69 @@ export interface VideoLocaleIds {
|
||||
portuguese?: string;
|
||||
}
|
||||
|
||||
// English types for animations
|
||||
export interface Dialogue {
|
||||
text: string;
|
||||
align: 'left' | 'right' | 'center';
|
||||
}
|
||||
|
||||
export interface CharacterPosition {
|
||||
x?: number;
|
||||
y?: number;
|
||||
z?: number;
|
||||
}
|
||||
|
||||
export interface SceneCommand {
|
||||
background?: string;
|
||||
character: string;
|
||||
position?: CharacterPosition;
|
||||
opacity?: number;
|
||||
startTime: number;
|
||||
finishTime?: number;
|
||||
dialogue?: Dialogue;
|
||||
}
|
||||
|
||||
export type Characters =
|
||||
| 'Alice'
|
||||
| 'Anna'
|
||||
| 'Bob'
|
||||
| 'Brian'
|
||||
| 'David'
|
||||
| 'Jake'
|
||||
| 'James'
|
||||
| 'Linda'
|
||||
| 'Lisa'
|
||||
| 'Maria'
|
||||
| 'Sarah'
|
||||
| 'Sophie'
|
||||
| 'Tom';
|
||||
|
||||
export interface SetupCharacter {
|
||||
character: Characters;
|
||||
position: CharacterPosition;
|
||||
opacity: number;
|
||||
isTalking?: boolean;
|
||||
}
|
||||
|
||||
export interface SetupAudio {
|
||||
filename: string;
|
||||
startTime: number;
|
||||
startTimestamp?: number;
|
||||
finishTimestamp?: number;
|
||||
}
|
||||
|
||||
export interface SceneSetup {
|
||||
background: string;
|
||||
characters: SetupCharacter[];
|
||||
audio: SetupAudio;
|
||||
alwaysShowDialogue?: boolean;
|
||||
}
|
||||
|
||||
export interface FullScene {
|
||||
setup: SceneSetup;
|
||||
commands: SceneCommand[];
|
||||
}
|
||||
|
||||
export interface PrerequisiteChallenge {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -146,6 +209,7 @@ export type ChallengeNode = {
|
||||
question: Question;
|
||||
assignments: string[];
|
||||
required: Required[];
|
||||
scene: FullScene;
|
||||
solutions: {
|
||||
[T in FileKey]: FileKeyChallenge;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
.character-wrap {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.character-wrap-hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.character-feature {
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
object-fit: cover;
|
||||
transform-origin: left;
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Characters, CharacterPosition } from '../../../../redux/prop-types';
|
||||
import { characterAssets } from './scene-assets';
|
||||
|
||||
import './character.css';
|
||||
|
||||
interface CharacterProps {
|
||||
position: CharacterPosition;
|
||||
opacity: number;
|
||||
name: Characters;
|
||||
isBlinking: boolean;
|
||||
isTalking: boolean;
|
||||
}
|
||||
|
||||
interface CharacterStyles {
|
||||
left?: string;
|
||||
top?: string;
|
||||
transform?: string;
|
||||
opacity?: number;
|
||||
}
|
||||
|
||||
export function Character({
|
||||
position,
|
||||
opacity,
|
||||
name,
|
||||
isBlinking,
|
||||
isTalking
|
||||
}: CharacterProps): JSX.Element {
|
||||
const [eyesAreOpen, setEyesAreOpen] = useState(true);
|
||||
const [mouthIsOpen, setMouthIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let blinkInterval: NodeJS.Timeout | null = null;
|
||||
let talkInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
if (isBlinking) {
|
||||
const msBetweenIntervals = Math.floor(Math.random() * 3000) + 2000;
|
||||
blinkInterval = setInterval(() => {
|
||||
const msBlinkDelay = Math.floor(Math.random() * 1000);
|
||||
setTimeout(() => {
|
||||
setEyesAreOpen(false);
|
||||
|
||||
setTimeout(() => {
|
||||
setEyesAreOpen(true);
|
||||
}, 30); // always unblink after 30ms
|
||||
}, msBlinkDelay);
|
||||
}, msBetweenIntervals);
|
||||
}
|
||||
|
||||
if (isTalking) {
|
||||
talkInterval = setInterval(() => {
|
||||
const openDuration = getRandomInt(100, 200);
|
||||
const closeDuration = getRandomInt(300, 400);
|
||||
|
||||
setTimeout(() => {
|
||||
setMouthIsOpen(true);
|
||||
}, openDuration);
|
||||
|
||||
setTimeout(() => {
|
||||
setMouthIsOpen(false);
|
||||
}, closeDuration);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function getRandomInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
// Clear intervals when component is unmounted or conditions change
|
||||
return () => {
|
||||
setEyesAreOpen(true);
|
||||
setMouthIsOpen(false);
|
||||
if (blinkInterval) clearInterval(blinkInterval);
|
||||
if (talkInterval) clearInterval(talkInterval);
|
||||
};
|
||||
}, [isBlinking, isTalking]);
|
||||
|
||||
const characterWrapStyles: CharacterStyles = {
|
||||
opacity
|
||||
};
|
||||
if (position.x) characterWrapStyles.left = `${position.x}%`;
|
||||
if (position.y) characterWrapStyles.top = `${position.y}%`;
|
||||
|
||||
const characterFeatureStyles: CharacterStyles = {
|
||||
transform: position.z
|
||||
? `translate(-${50 * position.z}%) scale(${position.z})`
|
||||
: `translate(-50%)`
|
||||
};
|
||||
|
||||
const { base, brows, eyesOpen, eyesClosed, mouthOpen, mouthClosed, glasses } =
|
||||
characterAssets[name];
|
||||
|
||||
return (
|
||||
<div style={characterWrapStyles} className='character-wrap'>
|
||||
<img
|
||||
style={characterFeatureStyles}
|
||||
className='character-feature'
|
||||
src={base}
|
||||
alt='Character Body'
|
||||
/>
|
||||
<img
|
||||
style={characterFeatureStyles}
|
||||
className='character-feature'
|
||||
src={brows}
|
||||
alt='Character Brows'
|
||||
/>
|
||||
<img
|
||||
style={characterFeatureStyles}
|
||||
className='character-feature'
|
||||
src={eyesAreOpen ? eyesOpen : eyesClosed}
|
||||
alt='Character Eyes'
|
||||
/>
|
||||
<img
|
||||
style={characterFeatureStyles}
|
||||
className='character-feature'
|
||||
src={mouthIsOpen ? mouthOpen : mouthClosed}
|
||||
alt='Character Mouth'
|
||||
/>
|
||||
{glasses && (
|
||||
<img
|
||||
style={characterFeatureStyles}
|
||||
className='character-feature'
|
||||
src={glasses}
|
||||
alt='Character Glasses'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Character.displayName = 'Character';
|
||||
|
||||
export default Character;
|
||||
@@ -0,0 +1,140 @@
|
||||
// TODO: get domain from env
|
||||
const domain = 'http://localhost:8080/';
|
||||
export const sounds = `${domain}/sounds`;
|
||||
export const images = `${domain}/images`;
|
||||
export const backgrounds = `${images}/backgrounds`;
|
||||
export const characters = `${images}/characters`;
|
||||
|
||||
const alice = `${characters}/alice`;
|
||||
const anna = `${characters}/anna`;
|
||||
const bob = `${characters}/bob`;
|
||||
const brian = `${characters}/brian`;
|
||||
const david = `${characters}/david`;
|
||||
const jake = `${characters}/jake`;
|
||||
const james = `${characters}/james`;
|
||||
const linda = `${characters}/linda`;
|
||||
const lisa = `${characters}/lisa`;
|
||||
const maria = `${characters}/maria`;
|
||||
const sarah = `${characters}/sarah`;
|
||||
const sophie = `${characters}/sophie`;
|
||||
const tom = `${characters}/tom`;
|
||||
|
||||
export const characterAssets = {
|
||||
Alice: {
|
||||
base: `${alice}/base.png`,
|
||||
brows: `${alice}/brows-normal.png`,
|
||||
eyesClosed: `${alice}/eyes-closed.png`,
|
||||
eyesOpen: `${alice}/eyes-open.png`,
|
||||
glasses: null,
|
||||
mouthClosed: `${alice}/mouth-smile.png`,
|
||||
mouthOpen: `${alice}/mouth-laugh.png`
|
||||
},
|
||||
Anna: {
|
||||
base: `${anna}/base.png`,
|
||||
brows: `${anna}/brows-normal.png`,
|
||||
eyesClosed: `${anna}/eyes-closed.png`,
|
||||
eyesOpen: `${anna}/eyes-open.png`,
|
||||
glasses: null,
|
||||
mouthClosed: `${anna}/mouth-smile.png`,
|
||||
mouthOpen: `${anna}/mouth-laugh.png`
|
||||
},
|
||||
Bob: {
|
||||
base: `${bob}/base.png`,
|
||||
brows: `${bob}/brows-normal.png`,
|
||||
eyesClosed: `${bob}/eyes-closed.png`,
|
||||
eyesOpen: `${bob}/eyes-open.png`,
|
||||
glasses: null,
|
||||
mouthClosed: `${bob}/mouth-smile.png`,
|
||||
mouthOpen: `${bob}/mouth-laugh.png`
|
||||
},
|
||||
Brian: {
|
||||
base: `${brian}/base.png`,
|
||||
brows: `${brian}/brows-neutral.png`,
|
||||
eyesClosed: `${brian}/eyes-closed.png`,
|
||||
eyesOpen: `${brian}/eyes-open.png`,
|
||||
glasses: `${brian}/glasses.png`,
|
||||
mouthClosed: `${brian}/mouth-smile.png`,
|
||||
mouthOpen: `${brian}/mouth-laugh.png`
|
||||
},
|
||||
David: {
|
||||
base: `${david}/base.png`,
|
||||
brows: `${david}/brows-normal.png`,
|
||||
eyesClosed: `${david}/eyes-closed.png`,
|
||||
eyesOpen: `${david}/eyes-open.png`,
|
||||
glasses: null,
|
||||
mouthClosed: `${david}/mouth-smile.png`,
|
||||
mouthOpen: `${david}/mouth-laugh.png`
|
||||
},
|
||||
Jake: {
|
||||
base: `${jake}/base.png`,
|
||||
brows: `${jake}/brows.png`,
|
||||
eyesClosed: `${jake}/eyes-closed.png`,
|
||||
eyesOpen: `${jake}/eyes-open.png`,
|
||||
glasses: null,
|
||||
mouthClosed: `${jake}/mouth-smile.png`,
|
||||
mouthOpen: `${jake}/mouth-laugh.png`
|
||||
},
|
||||
James: {
|
||||
base: `${james}/base.png`,
|
||||
brows: `${james}/brows-normal.png`,
|
||||
eyesClosed: `${james}/eyes-closed.png`,
|
||||
eyesOpen: `${james}/eyes-open.png`,
|
||||
glasses: `${james}/glasses.png`,
|
||||
mouthClosed: `${james}/mouth-smile.png`,
|
||||
mouthOpen: `${james}/mouth-laugh.png`
|
||||
},
|
||||
Linda: {
|
||||
base: `${linda}/base.png`,
|
||||
brows: `${linda}/brows-normal.png`,
|
||||
eyesClosed: `${linda}/eyes-closed.png`,
|
||||
eyesOpen: `${linda}/eyes-open.png`,
|
||||
glasses: null,
|
||||
mouthClosed: `${linda}/mouth-smile.png`,
|
||||
mouthOpen: `${linda}/mouth-laugh.png`
|
||||
},
|
||||
Lisa: {
|
||||
base: `${lisa}/base.png`,
|
||||
brows: `${lisa}/brows-normal.png`,
|
||||
eyesClosed: `${lisa}/eyes-closed.png`,
|
||||
eyesOpen: `${lisa}/eyes-open.png`,
|
||||
glasses: null,
|
||||
mouthClosed: `${lisa}/mouth-smile.png`,
|
||||
mouthOpen: `${lisa}/mouth-laugh.png`
|
||||
},
|
||||
Maria: {
|
||||
base: `${maria}/base.png`,
|
||||
brows: `${maria}/brows-normal.png`,
|
||||
eyesClosed: `${maria}/eyes-closed.png`,
|
||||
eyesOpen: `${maria}/eyes-open.png`,
|
||||
glasses: `${maria}/glasses.png`,
|
||||
mouthClosed: `${maria}/mouth-smile.png`,
|
||||
mouthOpen: `${maria}/mouth-laugh.png`
|
||||
},
|
||||
Sarah: {
|
||||
base: `${sarah}/base.png`,
|
||||
brows: `${sarah}/brows-normal.png`,
|
||||
eyesClosed: `${sarah}/eyes-closed.png`,
|
||||
eyesOpen: `${sarah}/eyes-open.png`,
|
||||
glasses: null,
|
||||
mouthClosed: `${sarah}/mouth-smile.png`,
|
||||
mouthOpen: `${sarah}/mouth-laugh.png`
|
||||
},
|
||||
Sophie: {
|
||||
base: `${sophie}/base.png`,
|
||||
brows: `${sophie}/brows-neutral.png`,
|
||||
eyesClosed: `${sophie}/eyes-closed.png`,
|
||||
eyesOpen: `${sophie}/eyes-open.png`,
|
||||
glasses: `${sophie}/glasses.png`,
|
||||
mouthClosed: `${sophie}/mouth-smile.png`,
|
||||
mouthOpen: `${sophie}/mouth-laugh.png`
|
||||
},
|
||||
Tom: {
|
||||
base: `${tom}/base.png`,
|
||||
brows: `${tom}/brows-normal.png`,
|
||||
eyesClosed: `${tom}/eyes-closed.png`,
|
||||
eyesOpen: `${tom}/eyes-open.png`,
|
||||
glasses: null,
|
||||
mouthClosed: `${tom}/mouth-smile.png`,
|
||||
mouthOpen: `${tom}/mouth-laugh.png`
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
.scene-wrapper {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scene-start-screen {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
background-color: rgba(10, 10, 35, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
color: var(--gray-00);
|
||||
}
|
||||
|
||||
.scene-start-btn,
|
||||
.scene-start-btn:hover,
|
||||
.scene-start-btn:focus,
|
||||
.scene-start-btn:active {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.scene-a11y-btn {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.scene-play-btn img {
|
||||
max-width: 75%;
|
||||
}
|
||||
|
||||
.scene-a11y-btn svg {
|
||||
width: calc(50px + 3vw);
|
||||
height: calc(50px + 3vw);
|
||||
}
|
||||
|
||||
.scene-dialogue-wrap {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: calc(5px + 0.25vw) calc(5px + 4vw);
|
||||
background: rgba(10, 10, 35, 0.9);
|
||||
font-size: calc(0.25vw + 0.75rem);
|
||||
width: 100%;
|
||||
min-height: calc(35px + 1vw + 2rem);
|
||||
}
|
||||
|
||||
.scene-dialogue-label {
|
||||
color: var(--blue-light);
|
||||
}
|
||||
|
||||
.scene-dialogue-align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.scene-dialogue-align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.scene-dialogue-align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.scene-dialogue-text {
|
||||
font-size: calc(0.25vw + 1rem);
|
||||
padding: 5px 10px;
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import React, { useEffect, useState, useRef } from 'react'; //, ReactElement } from 'react';
|
||||
import { Col } from '@freecodecamp/ui';
|
||||
import { FullScene } from '../../../../redux/prop-types';
|
||||
import { Loader } from '../../../../components/helpers';
|
||||
import AccessibilityIcon from '../../../../assets/icons/accessibility';
|
||||
import { sounds, images, backgrounds } from './scene-assets';
|
||||
import Character from './character';
|
||||
|
||||
import './scene.css';
|
||||
|
||||
export function Scene({ scene }: { scene: FullScene }): JSX.Element {
|
||||
const { setup, commands } = scene;
|
||||
const { audio, alwaysShowDialogue } = setup;
|
||||
|
||||
const audioTimestamp =
|
||||
audio.startTimestamp !== null && audio.finishTimestamp !== null
|
||||
? `#t=${audio.startTimestamp},${audio.finishTimestamp}`
|
||||
: '';
|
||||
|
||||
const audioRef = useRef<HTMLAudioElement>(
|
||||
new Audio(`${sounds}/${audio.filename}${audioTimestamp}`)
|
||||
);
|
||||
|
||||
// on mount
|
||||
useEffect(() => {
|
||||
const { current } = audioRef;
|
||||
current.addEventListener('canplaythrough', audioLoaded);
|
||||
|
||||
// on unmount
|
||||
return () => {
|
||||
const { current } = audioRef;
|
||||
|
||||
current.pause();
|
||||
current.currentTime = 0;
|
||||
current.removeEventListener('canplaythrough', audioLoaded);
|
||||
};
|
||||
}, [audioRef]);
|
||||
|
||||
const audioLoaded = () => {
|
||||
setSceneIsReady(true);
|
||||
};
|
||||
|
||||
const initBackground = setup.background;
|
||||
const initDialogue = { label: '', text: '', align: 'left' };
|
||||
const initCharacters = setup.characters.map(character => {
|
||||
return {
|
||||
...character,
|
||||
opacity: character.opacity ?? 1,
|
||||
isTalking: false
|
||||
};
|
||||
});
|
||||
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [sceneIsReady, setSceneIsReady] = useState(true);
|
||||
const [showDialogue, setShowDialogue] = useState(false);
|
||||
const [accessibilityOn, setAccessibilityOn] = useState(false);
|
||||
const [characters, setCharacters] = useState(initCharacters);
|
||||
const [dialogue, setDialogue] = useState(initDialogue);
|
||||
const [background, setBackground] = useState(initBackground);
|
||||
|
||||
const playScene = () => {
|
||||
setIsPlaying(true);
|
||||
setShowDialogue(true);
|
||||
|
||||
commands.forEach((command, commandIndex) => {
|
||||
// Start audio timeout
|
||||
setTimeout(function () {
|
||||
void audioRef.current.play();
|
||||
}, audio.startTime * 1000);
|
||||
|
||||
// Start command timeout
|
||||
setTimeout(() => {
|
||||
if (command.background) setBackground(command.background);
|
||||
|
||||
setDialogue(
|
||||
command.dialogue
|
||||
? { ...command.dialogue, label: command.character }
|
||||
: initDialogue
|
||||
);
|
||||
|
||||
setCharacters(prevCharacters => {
|
||||
const newCharacters = prevCharacters.map(character => {
|
||||
if (character.character === command.character) {
|
||||
return {
|
||||
...character,
|
||||
position: command.position ?? character.position,
|
||||
opacity: command.opacity ?? character.opacity,
|
||||
isTalking: command.dialogue ? true : false
|
||||
};
|
||||
}
|
||||
return character;
|
||||
});
|
||||
return newCharacters;
|
||||
});
|
||||
}, command.startTime * 1000);
|
||||
|
||||
// Finish command timeout, only used when there's a dialogue
|
||||
if (command.dialogue) {
|
||||
setTimeout(
|
||||
() => {
|
||||
setCharacters(prevCharacters => {
|
||||
const newCharacters = prevCharacters.map(character => {
|
||||
if (character.character === command.character) {
|
||||
return {
|
||||
...character,
|
||||
isTalking: false
|
||||
};
|
||||
}
|
||||
return character;
|
||||
});
|
||||
return newCharacters;
|
||||
});
|
||||
},
|
||||
(command.finishTime as number) * 1000
|
||||
);
|
||||
}
|
||||
|
||||
// Last command timeout
|
||||
if (commandIndex === commands.length - 1) {
|
||||
setTimeout(
|
||||
() => {
|
||||
finishScene();
|
||||
},
|
||||
command.finishTime
|
||||
? command.finishTime * 1000 + 500
|
||||
: command.startTime * 1000 + 500
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const finishScene = () => {
|
||||
audioRef.current.pause();
|
||||
audioRef.current.src = `${sounds}/${audio.filename}${audioTimestamp}`;
|
||||
audioRef.current.currentTime = audio.startTimestamp || 0;
|
||||
setIsPlaying(false);
|
||||
setShowDialogue(false);
|
||||
setDialogue(initDialogue);
|
||||
setCharacters(initCharacters);
|
||||
setBackground(initBackground);
|
||||
};
|
||||
|
||||
return (
|
||||
<Col lg={10} lgOffset={1} md={10} mdOffset={1}>
|
||||
<div
|
||||
className='scene-wrapper'
|
||||
style={{
|
||||
backgroundImage: `url("${backgrounds}/${background}")`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center center',
|
||||
aspectRatio: '16 / 9'
|
||||
}}
|
||||
>
|
||||
{!sceneIsReady ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<>
|
||||
{characters.map(
|
||||
(
|
||||
{ character, position = {}, opacity = 1, isTalking = false },
|
||||
i
|
||||
) => {
|
||||
return (
|
||||
<Character
|
||||
key={i}
|
||||
name={character}
|
||||
position={position}
|
||||
opacity={opacity}
|
||||
isTalking={isTalking}
|
||||
isBlinking={isPlaying}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
|
||||
{showDialogue && (alwaysShowDialogue || accessibilityOn) && (
|
||||
<div
|
||||
className={`scene-dialogue-wrap ${
|
||||
dialogue.align ? `scene-dialogue-align-${dialogue.align}` : ''
|
||||
}`}
|
||||
>
|
||||
<div className='scene-dialogue-label'>{dialogue.label}</div>
|
||||
<div className='scene-dialogue-text'>{dialogue.text}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isPlaying && (
|
||||
<div className='scene-start-screen'>
|
||||
<button
|
||||
className='scene-start-btn scene-play-btn'
|
||||
onClick={playScene}
|
||||
>
|
||||
<img src={`${images}/play-button.png`} alt='Press Play' />
|
||||
</button>
|
||||
|
||||
{!alwaysShowDialogue && (
|
||||
<button
|
||||
className='scene-start-btn scene-a11y-btn'
|
||||
aria-label='Accessibility On/Off'
|
||||
onClick={() => setAccessibilityOn(!accessibilityOn)}
|
||||
>
|
||||
<AccessibilityIcon
|
||||
fill={
|
||||
accessibilityOn ? 'var(--gray-00)' : 'var(--gray-15)'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
Scene.displayName = 'Scene';
|
||||
|
||||
export default Scene;
|
||||
@@ -13,12 +13,10 @@ import { createSelector } from 'reselect';
|
||||
import { Container, Col, Row } from '@freecodecamp/ui';
|
||||
|
||||
// Local Utilities
|
||||
import Loader from '../../../components/helpers/loader';
|
||||
import Spacer from '../../../components/helpers/spacer';
|
||||
import LearnLayout from '../../../components/layouts/learn';
|
||||
import { ChallengeNode, ChallengeMeta } from '../../../redux/prop-types';
|
||||
import Hotkeys from '../components/hotkeys';
|
||||
import VideoPlayer from '../components/video-player';
|
||||
import CompletionModal from '../components/completion-modal';
|
||||
import HelpModal from '../components/help-modal';
|
||||
import PrismFormatted from '../components/prism-formatted';
|
||||
@@ -28,6 +26,7 @@ import {
|
||||
openModal
|
||||
} from '../redux/actions';
|
||||
import { isChallengeCompletedSelector } from '../redux/selectors';
|
||||
import Scene from '../components/scene/scene';
|
||||
|
||||
// Styles
|
||||
import '../odin/show.css';
|
||||
@@ -180,9 +179,9 @@ class ShowDialogue extends Component<ShowDialogueProps, ShowDialogueState> {
|
||||
description,
|
||||
superBlock,
|
||||
block,
|
||||
videoId,
|
||||
fields: { blockName },
|
||||
assignments
|
||||
assignments,
|
||||
scene
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -210,29 +209,16 @@ class ShowDialogue extends Component<ShowDialogueProps, ShowDialogueState> {
|
||||
/>
|
||||
<Container>
|
||||
<Row>
|
||||
{videoId && (
|
||||
<Col lg={10} lgOffset={1} md={10} mdOffset={1}>
|
||||
<Spacer size='medium' />
|
||||
<div className='video-wrapper'>
|
||||
{!this.state.videoIsLoaded ? (
|
||||
<div className='video-placeholder-loader'>
|
||||
<Loader />
|
||||
</div>
|
||||
) : null}
|
||||
<VideoPlayer
|
||||
onVideoLoad={this.onVideoLoad}
|
||||
title={title}
|
||||
videoId={videoId}
|
||||
videoIsLoaded={this.state.videoIsLoaded}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
)}
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Spacer size='medium' />
|
||||
<h2>{title}</h2>
|
||||
<PrismFormatted className={'line-numbers'} text={description} />
|
||||
<Spacer size='medium' />
|
||||
</Col>
|
||||
|
||||
<Scene scene={scene} />
|
||||
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<ObserveKeys>
|
||||
<h2>{t('learn.assignments')}</h2>
|
||||
<div className='video-quiz-options'>
|
||||
@@ -328,6 +314,43 @@ export const query = graphql`
|
||||
}
|
||||
translationPending
|
||||
assignments
|
||||
scene {
|
||||
setup {
|
||||
background
|
||||
characters {
|
||||
character
|
||||
position {
|
||||
x
|
||||
y
|
||||
z
|
||||
}
|
||||
opacity
|
||||
}
|
||||
audio {
|
||||
filename
|
||||
startTime
|
||||
startTimestamp
|
||||
finishTimestamp
|
||||
}
|
||||
alwaysShowDialogue
|
||||
}
|
||||
commands {
|
||||
background
|
||||
character
|
||||
position {
|
||||
x
|
||||
y
|
||||
z
|
||||
}
|
||||
opacity
|
||||
startTime
|
||||
finishTime
|
||||
dialogue {
|
||||
text
|
||||
align
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import { isChallengeCompletedSelector } from '../redux/selectors';
|
||||
// Styles
|
||||
import '../video.css';
|
||||
import './show.css';
|
||||
import Scene from '../components/scene/scene';
|
||||
|
||||
// Redux Setup
|
||||
const mapStateToProps = createSelector(
|
||||
@@ -254,7 +255,8 @@ class ShowFillInTheBlank extends Component<
|
||||
translationPending,
|
||||
fields: { blockName },
|
||||
fillInTheBlank: { sentence, blanks },
|
||||
audioPath
|
||||
audioPath,
|
||||
scene
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -312,6 +314,11 @@ class ShowFillInTheBlank extends Component<
|
||||
</audio>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{scene && <Scene scene={scene} />}
|
||||
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Spacer size='medium' />
|
||||
<PrismFormatted text={instructions} />
|
||||
<Spacer size='medium' />
|
||||
@@ -430,6 +437,43 @@ export const query = graphql`
|
||||
feedback
|
||||
}
|
||||
}
|
||||
scene {
|
||||
setup {
|
||||
background
|
||||
characters {
|
||||
character
|
||||
position {
|
||||
x
|
||||
y
|
||||
z
|
||||
}
|
||||
opacity
|
||||
}
|
||||
audio {
|
||||
filename
|
||||
startTime
|
||||
startTimestamp
|
||||
finishTimestamp
|
||||
}
|
||||
alwaysShowDialogue
|
||||
}
|
||||
commands {
|
||||
background
|
||||
character
|
||||
position {
|
||||
x
|
||||
y
|
||||
z
|
||||
}
|
||||
opacity
|
||||
startTime
|
||||
finishTime
|
||||
dialogue {
|
||||
text
|
||||
align
|
||||
}
|
||||
}
|
||||
}
|
||||
translationPending
|
||||
audioPath
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import Hotkeys from '../components/hotkeys';
|
||||
import VideoPlayer from '../components/video-player';
|
||||
import CompletionModal from '../components/completion-modal';
|
||||
import HelpModal from '../components/help-modal';
|
||||
import Scene from '../components/scene/scene';
|
||||
import PrismFormatted from '../components/prism-formatted';
|
||||
import {
|
||||
challengeMounted,
|
||||
@@ -217,7 +218,8 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
|
||||
fields: { blockName },
|
||||
question: { text, answers, solution },
|
||||
assignments,
|
||||
audioPath
|
||||
audioPath,
|
||||
scene
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -292,6 +294,11 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
|
||||
</>
|
||||
)}
|
||||
<Spacer size='medium' />
|
||||
</Col>
|
||||
|
||||
{scene && <Scene scene={scene} />}
|
||||
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<ObserveKeys>
|
||||
{assignments.length > 0 && (
|
||||
<>
|
||||
@@ -456,6 +463,43 @@ export const query = graphql`
|
||||
}
|
||||
solution
|
||||
}
|
||||
scene {
|
||||
setup {
|
||||
background
|
||||
characters {
|
||||
character
|
||||
position {
|
||||
x
|
||||
y
|
||||
z
|
||||
}
|
||||
opacity
|
||||
}
|
||||
audio {
|
||||
filename
|
||||
startTime
|
||||
startTimestamp
|
||||
finishTimestamp
|
||||
}
|
||||
alwaysShowDialogue
|
||||
}
|
||||
commands {
|
||||
background
|
||||
character
|
||||
position {
|
||||
x
|
||||
y
|
||||
z
|
||||
}
|
||||
opacity
|
||||
startTime
|
||||
finishTime
|
||||
dialogue {
|
||||
text
|
||||
align
|
||||
}
|
||||
}
|
||||
}
|
||||
translationPending
|
||||
assignments
|
||||
audioPath
|
||||
|
||||
+130
-3
@@ -1,15 +1,142 @@
|
||||
---
|
||||
id: 651dd3e06ffb500e3f2ce478
|
||||
title: "Dialogue 1: Maria Introduces Herself to Tom"
|
||||
title: 'Dialogue 1: Maria Introduces Herself to Tom'
|
||||
challengeType: 21
|
||||
videoId: nLDychdBwUg
|
||||
dashedName: dialogue-1-maria-introduces-herself-to-tom
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
Watch the video above to understand the context of the upcoming lessons.
|
||||
Watch the video below to understand the context of the upcoming lessons.
|
||||
|
||||
# --assignment--
|
||||
|
||||
Watch the video
|
||||
|
||||
# --scene--
|
||||
|
||||
```json
|
||||
{
|
||||
"setup": {
|
||||
"background": "company2-center.png",
|
||||
"characters": [
|
||||
{
|
||||
"character": "Maria",
|
||||
"position": { "x": -25, "y": 0, "z": 1 }
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"position": { "x": 125, "y": 0, "z": 1 }
|
||||
}
|
||||
],
|
||||
"audio": {
|
||||
"filename": "1.1-1.mp3",
|
||||
"startTime": 1
|
||||
},
|
||||
"alwaysShowDialogue": true
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"character": "Maria",
|
||||
"position": { "x": 25, "y": 0, "z": 1 },
|
||||
"startTime": 0
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"position": { "x": 70, "y": 0, "z": 1 },
|
||||
"startTime": 0.5
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"startTime": 1,
|
||||
"finishTime": 4.65,
|
||||
"dialogue": {
|
||||
"text": "Hello! You're the new graphic designer, right? I'm Maria, the team lead.",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 5.1,
|
||||
"finishTime": 9.1,
|
||||
"dialogue": {
|
||||
"text": "Hi, that's right! I'm Tom McKenzie. It's a pleasure to meet you.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"startTime": 9.7,
|
||||
"finishTime": 12.6,
|
||||
"dialogue": {
|
||||
"text": "Welcome aboard, Tom! How do you like California so far?",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 12.7,
|
||||
"finishTime": 15.7,
|
||||
"dialogue": {
|
||||
"text": "I like it. It's different from Texas, but I like it here.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"startTime": 16.2,
|
||||
"finishTime": 18.5,
|
||||
"dialogue": {
|
||||
"text": "Great! Let me show you to your desk.",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"startTime": 18.7,
|
||||
"finishTime": 21.2,
|
||||
"dialogue": {
|
||||
"text": "Do you see the desk with a drawing tablet and a computer?",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"startTime": 21.4,
|
||||
"finishTime": 22.6,
|
||||
"dialogue": {
|
||||
"text": "That's your workspace.",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 22.8,
|
||||
"finishTime": 24.5,
|
||||
"dialogue": {
|
||||
"text": "Everything looks great.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 24.5,
|
||||
"finishTime": 26.1,
|
||||
"dialogue": {
|
||||
"text": "Thanks for showing me around the place, Maria.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"position": { "x": 125, "y": 0, "z": 1 },
|
||||
"startTime": 26.6
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"position": { "x": -25, "y": 0, "z": 1 },
|
||||
"startTime": 27.1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
+50
-7
@@ -3,30 +3,73 @@ id: 651dd5296ffb500e3f2ce479
|
||||
title: Task 1
|
||||
challengeType: 22
|
||||
dashedName: task-1
|
||||
audioPath: 1.1-1.mp3#t=1.6,3.3
|
||||
---
|
||||
|
||||
<!--
|
||||
AUDIO REFERENCE:
|
||||
Maria: Hello! You are the new graphic designer, right?
|
||||
Maria: Hello! You're the new graphic designer, right?
|
||||
-->
|
||||
|
||||
# --description--
|
||||
|
||||
In English, when making introductions or identifying someone, you use the verb `to be`. In this case, `You are` is used to address the person Maria is talking to and affirmatively identify their occupation.
|
||||
In English, contractions are commonly used to make speech sound more natural and fluent. `You're` is a contraction of `you are`.
|
||||
|
||||
Maria is introducing herself and confirming Tom's job role. `Are` is used in the present affirmative to make a statement.
|
||||
This contraction is a combination of the pronoun `you` and the verb `are`, which is part of the verb `to be`. `Are` is used here in the present affirmative to make a statement or ask a question. This is a typical way to confirm someone's role or identity in English.
|
||||
|
||||
# --fillInTheBlank--
|
||||
|
||||
## --sentence--
|
||||
|
||||
`Hello, You _ the new graphic designer, right?`
|
||||
`Hello, You_ the new graphic designer, right?`
|
||||
|
||||
## --blanks--
|
||||
|
||||
`are`
|
||||
`'re`
|
||||
|
||||
### --feedback--
|
||||
|
||||
The verb `to be` is an irregular verb. When conjugated with the pronoun `you`, `be` becomes `are`. For example: `You are an English learner.`
|
||||
In the audio, `you're` is used, which is a contraction of `you are`. The verb `to be` is irregular. When conjugated with `you`, it becomes `are`, as in `You are an English learner.` Here, `you're` expresses the same meaning.
|
||||
|
||||
# --scene--
|
||||
|
||||
```json
|
||||
{
|
||||
"setup": {
|
||||
"background": "company2-center.png",
|
||||
"characters": [
|
||||
{
|
||||
"character": "Maria",
|
||||
"position": { "x": 50, "y": 0, "z": 1.5 },
|
||||
"opacity": 0
|
||||
}
|
||||
],
|
||||
"audio": {
|
||||
"filename": "1.1-1.mp3",
|
||||
"startTime": 1,
|
||||
"startTimestamp": 0,
|
||||
"finishTimestamp": 2.5
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"character": "Maria",
|
||||
"opacity": 1,
|
||||
"startTime": 0
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"startTime": 0.5,
|
||||
"finishTime": 2.9,
|
||||
"dialogue": {
|
||||
"text": "Hello! You're the new graphic designer, right?",
|
||||
"align": "center"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"opacity": 0,
|
||||
"startTime": 3.4
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
+46
-11
@@ -3,7 +3,6 @@ id: 651dd5386ffb500e3f2ce47a
|
||||
title: Task 2
|
||||
challengeType: 22
|
||||
dashedName: task-2
|
||||
audioPath: curriculum/js-music-player/We-Are-Going-to-Make-it.mp3
|
||||
---
|
||||
|
||||
<!--
|
||||
@@ -13,26 +12,62 @@ Maria: Hello! You are the new graphic designer, right?
|
||||
|
||||
# --description--
|
||||
|
||||
In English, to check or confirm something people sometimes use tag questions. For example, `You are a programmer, right?` Here, `right?` is used as a tag to check or confirm the previous statement.
|
||||
In English, to check or confirm something people sometimes use tag questions. For example, `You're a programmer, right?` Here, `right?` is used as a tag to check or confirm the previous statement.
|
||||
|
||||
# --fillInTheBlank--
|
||||
|
||||
## --sentence--
|
||||
|
||||
`Hello, You _ the new graphic designer, _?`
|
||||
`Hello, You're the new graphic designer, _?`
|
||||
|
||||
## --blanks--
|
||||
|
||||
`are`
|
||||
|
||||
### --feedback--
|
||||
|
||||
Pay attention to the verb in the sentence.
|
||||
|
||||
---
|
||||
|
||||
`right`
|
||||
|
||||
### --feedback--
|
||||
|
||||
Pay attention to the verb in the sentence.
|
||||
|
||||
# --scene--
|
||||
|
||||
```json
|
||||
{
|
||||
"setup": {
|
||||
"background": "company2-center.png",
|
||||
"characters": [
|
||||
{
|
||||
"character": "Maria",
|
||||
"position": { "x": 50, "y": 0, "z": 1.5 },
|
||||
"opacity": 0
|
||||
}
|
||||
],
|
||||
"audio": {
|
||||
"filename": "1.1-1.mp3",
|
||||
"startTime": 1,
|
||||
"startTimestamp": 0,
|
||||
"finishTimestamp": 2.5
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"character": "Maria",
|
||||
"opacity": 1,
|
||||
"startTime": 0
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"startTime": 0.5,
|
||||
"finishTime": 2.9,
|
||||
"dialogue": {
|
||||
"text": "Hello! You're the new graphic designer, right?",
|
||||
"align": "center"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"opacity": 0,
|
||||
"startTime": 3.4
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
+44
-1
@@ -3,7 +3,6 @@ id: 6537e6ece93e5724eeb27c54
|
||||
title: Task 3
|
||||
challengeType: 19
|
||||
dashedName: task-3
|
||||
audioPath: curriculum/js-music-player/We-Are-Going-to-Make-it.mp3
|
||||
---
|
||||
|
||||
<!--
|
||||
@@ -56,3 +55,47 @@ Focus on the term Maria used to describe herself.
|
||||
## --video-solution--
|
||||
|
||||
3
|
||||
|
||||
# --scene--
|
||||
|
||||
```json
|
||||
{
|
||||
"setup": {
|
||||
"background": "company2-center.png",
|
||||
"characters": [
|
||||
{
|
||||
"character": "Maria",
|
||||
"position": { "x": 50, "y": 0, "z": 1.5 },
|
||||
"opacity": 0
|
||||
}
|
||||
],
|
||||
"audio": {
|
||||
"filename": "1.1-1.mp3",
|
||||
"startTime": 1,
|
||||
"startTimestamp": 2.6,
|
||||
"finishTimestamp": 4
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"character": "Maria",
|
||||
"opacity": 1,
|
||||
"startTime": 0
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"startTime": 0.5,
|
||||
"finishTime": 2.2,
|
||||
"dialogue": {
|
||||
"text": "I'm Maria, the team lead.",
|
||||
"align": "center"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"opacity": 0,
|
||||
"startTime": 2.7
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
+44
-1
@@ -3,7 +3,6 @@ id: 6543aa3df5f028dba112f275
|
||||
title: Task 4
|
||||
challengeType: 22
|
||||
dashedName: task-4
|
||||
audioPath: curriculum/js-music-player/We-Are-Going-to-Make-it.mp3
|
||||
---
|
||||
|
||||
<!--
|
||||
@@ -36,3 +35,47 @@ Focus on the term Maria used to describe herself.
|
||||
### --feedback--
|
||||
|
||||
Focus on the term Maria used to describe herself.
|
||||
|
||||
# --scene--
|
||||
|
||||
```json
|
||||
{
|
||||
"setup": {
|
||||
"background": "company2-center.png",
|
||||
"characters": [
|
||||
{
|
||||
"character": "Maria",
|
||||
"position": { "x": 50, "y": 0, "z": 1.5 },
|
||||
"opacity": 0
|
||||
}
|
||||
],
|
||||
"audio": {
|
||||
"filename": "1.1-1.mp3",
|
||||
"startTime": 1,
|
||||
"startTimestamp": 2.6,
|
||||
"finishTimestamp": 4
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"character": "Maria",
|
||||
"opacity": 1,
|
||||
"startTime": 0
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"startTime": 0.5,
|
||||
"finishTime": 2.2,
|
||||
"dialogue": {
|
||||
"text": "I'm Maria, the team lead.",
|
||||
"align": "center"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"opacity": 0,
|
||||
"startTime": 2.7
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
-1
@@ -3,7 +3,6 @@ id: 6543aaa9f5f028dba112f276
|
||||
title: Task 5
|
||||
challengeType: 19
|
||||
dashedName: task-5
|
||||
audioPath: curriculum/js-music-player/We-Are-Going-to-Make-it.mp3
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
+219
-2
@@ -3,13 +3,230 @@ id: 656a2a7b05241026c429e3f0
|
||||
title: "Dialogue 2: Tom Meets the Coworker Next To Him"
|
||||
challengeType: 21
|
||||
dashedName: dialogue-2-tom-meets-the-coworker-next-to-him
|
||||
videoId: nLDychdBwUg
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
Watch the video above to understand the context of the upcoming lessons.
|
||||
Watch the video below to understand the context of the upcoming lessons.
|
||||
|
||||
# --assignment--
|
||||
|
||||
Watch the video
|
||||
|
||||
# --scene--
|
||||
|
||||
```json
|
||||
{
|
||||
"setup": {
|
||||
"background": "company2-center.png",
|
||||
"characters": [
|
||||
{
|
||||
"character": "Tom",
|
||||
"position": { "x": -25, "y": 0, "z": 1 }
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"position": { "x": 125, "y": 0, "z": 1 }
|
||||
}
|
||||
],
|
||||
"audio": {
|
||||
"filename": "1.1-2.mp3",
|
||||
"startTime": 1
|
||||
},
|
||||
"alwaysShowDialogue": true
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"character": "Tom",
|
||||
"position": { "x": 25, "y": 0, "z": 1 },
|
||||
"startTime": 0
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"position": { "x": 70, "y": 0, "z": 1 },
|
||||
"startTime": 0.5
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 1,
|
||||
"finishTime": 4.3,
|
||||
"dialogue": {
|
||||
"text": "Hi, there. I'm Tom. I'm the new graphic designer.",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 4.9,
|
||||
"finishTime": 7.7,
|
||||
"dialogue": {
|
||||
"text": "Oh, hi Tom! I'm Sophie. I'm a developer.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 7.8,
|
||||
"finishTime": 8.8,
|
||||
"dialogue": {
|
||||
"text": "Where are you from, Tom?",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 8.9,
|
||||
"finishTime": 11.1,
|
||||
"dialogue": {
|
||||
"text": "I'm from Texas. How about you?",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 11.5,
|
||||
"finishTime": 13.3,
|
||||
"dialogue": {
|
||||
"text": "I'm from here in California. Welcome aboard.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 13.7,
|
||||
"finishTime": 16.4,
|
||||
"dialogue": {
|
||||
"text": "Thanks. Everybody is so nice around here.",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 16.6,
|
||||
"finishTime": 20.6,
|
||||
"dialogue": {
|
||||
"text": "Hey, is this one of those standing desks? These are great!",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 20.7,
|
||||
"finishTime": 23.7,
|
||||
"dialogue": {
|
||||
"text": "It's good to stand up a little instead of just sitting all the time.",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 23.9,
|
||||
"finishTime": 27.3,
|
||||
"dialogue": {
|
||||
"text": "That's so true. I'm a bit inactive, sitting all the time.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 27.6,
|
||||
"finishTime": 29.1,
|
||||
"dialogue": {
|
||||
"text": "This is a good alternative for me.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 29.7,
|
||||
"finishTime": 32.6,
|
||||
"dialogue": {
|
||||
"text": "But hey, now your desk is just like my desk. You're in luck.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 33,
|
||||
"finishTime": 36.5,
|
||||
"dialogue": {
|
||||
"text": "Oh, awesome. My computer and drawing tablet are great, too.",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 37.5,
|
||||
"finishTime": 40.4,
|
||||
"dialogue": {
|
||||
"text": "Yeah. At this company, they're very attentive to these details.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 40.4,
|
||||
"finishTime": 44.4,
|
||||
"dialogue": {
|
||||
"text": "You're going to like it here if you're into cutting-edge gadgets.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 44.5,
|
||||
"finishTime": 50.2,
|
||||
"dialogue": {
|
||||
"text": "This is so cool. A standing desk, an ergonomic chair and an ergonomic mouse.",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 50.2,
|
||||
"finishTime": 53.7,
|
||||
"dialogue": {
|
||||
"text": "Man, everything is perfect. I'm in love with this place!",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 53.8,
|
||||
"finishTime": 56.8,
|
||||
"dialogue": {
|
||||
"text": "So nice to have someone so energetic like you in the team.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 56.8,
|
||||
"finishTime": 58,
|
||||
"dialogue": {
|
||||
"text": "Are you ready to begin?",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 58,
|
||||
"finishTime": 60,
|
||||
"dialogue": {
|
||||
"text": "Yes, I sure am!",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"position": { "x": 125, "y": 0, "z": 1 },
|
||||
"startTime": 60.5
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"position": { "x": -25, "y": 0, "z": 1 },
|
||||
"startTime": 61
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
+166
-3
@@ -1,15 +1,178 @@
|
||||
---
|
||||
id: 656cbad538b114095fb14c0e
|
||||
title: "Dialogue 3: Tom and Sophie Take a Lunch Break"
|
||||
title: 'Dialogue 3: Tom and Sophie Take a Lunch Break'
|
||||
challengeType: 21
|
||||
dashedName: dialogue-3-tom-and-sophie-take-a-lunch-break
|
||||
videoId: nLDychdBwUg
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
What the video above to understand the context of the upcoming lessons.
|
||||
What the video below to understand the context of the upcoming lessons.
|
||||
|
||||
# --assignment--
|
||||
|
||||
Watch the video
|
||||
|
||||
# --scene--
|
||||
|
||||
```json
|
||||
{
|
||||
"setup": {
|
||||
"background": "company2-center.png",
|
||||
"characters": [
|
||||
{
|
||||
"character": "Tom",
|
||||
"position": { "x": -25, "y": 0, "z": 1 }
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"position": { "x": 125, "y": 0, "z": 1 }
|
||||
}
|
||||
],
|
||||
"audio": {
|
||||
"filename": "1.1-3.mp3",
|
||||
"startTime": 1.2
|
||||
},
|
||||
"alwaysShowDialogue": true
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"character": "Tom",
|
||||
"position": { "x": 25, "y": 0, "z": 1 },
|
||||
"startTime": 0
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"position": { "x": 70, "y": 0, "z": 1 },
|
||||
"startTime": 0.5
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 1,
|
||||
"finishTime": 4.4,
|
||||
"dialogue": {
|
||||
"text": "Wow, I'm so hungry. Is it lunch time?",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 5,
|
||||
"finishTime": 6.4,
|
||||
"dialogue": {
|
||||
"text": "Yes, it is.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 6.6,
|
||||
"finishTime": 8.5,
|
||||
"dialogue": {
|
||||
"text": "Are you eating here or are you going out?",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 9.1,
|
||||
"finishTime": 10.8,
|
||||
"dialogue": {
|
||||
"text": "Today, I'm going out.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 11,
|
||||
"finishTime": 14,
|
||||
"dialogue": {
|
||||
"text": "I can show you some places around here. Are you interested?",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 14,
|
||||
"finishTime": 17.2,
|
||||
"dialogue": {
|
||||
"text": "Of course. Any favorite lunch spot around here, Sophie?",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 17.4,
|
||||
"finishTime": 20.5,
|
||||
"dialogue": {
|
||||
"text": "I know a nice one. It's a café right down the street.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 20.5,
|
||||
"finishTime": 21.3,
|
||||
"dialogue": {
|
||||
"text": "Is that OK for you?",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 21.5,
|
||||
"finishTime": 22.7,
|
||||
"dialogue": {
|
||||
"text": "Sounds great!",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 23.1,
|
||||
"finishTime": 25.2,
|
||||
"dialogue": {
|
||||
"text": "Yeah. It's nice to have a break from the office.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"startTime": 25.4,
|
||||
"finishTime": 28.7,
|
||||
"dialogue": {
|
||||
"text": "It is. Is the café within walking distance?",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 29.1,
|
||||
"finishTime": 30.4,
|
||||
"dialogue": {
|
||||
"text": "Well, it's not far.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 30.5,
|
||||
"finishTime": 32.1,
|
||||
"dialogue": {
|
||||
"text": "Come on. We can go together.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"position": { "x": 125, "y": 0, "z": 1 },
|
||||
"startTime": 32.6
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"position": { "x": -25, "y": 0, "z": 1 },
|
||||
"startTime": 33.1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
+154
-2
@@ -1,9 +1,8 @@
|
||||
---
|
||||
id: 656cd4b014d03a1baf452429
|
||||
title: "Dialogue 4: Sophie Introduces Brian to Tom"
|
||||
title: 'Dialogue 4: Sophie Introduces Brian to Tom'
|
||||
challengeType: 21
|
||||
dashedName: dialogue-4-sophie-introduces-brian-to-tom
|
||||
videoId: nLDychdBwUg
|
||||
---
|
||||
|
||||
# --description--
|
||||
@@ -13,3 +12,156 @@ What the video above to understand the context of the upcoming lessons.
|
||||
# --assignment--
|
||||
|
||||
Watch the video
|
||||
|
||||
# --scene--
|
||||
|
||||
```json
|
||||
{
|
||||
"setup": {
|
||||
"background": "cafe.png",
|
||||
"characters": [
|
||||
{
|
||||
"character": "Tom",
|
||||
"position": { "x": -25, "y": 0, "z": 1 }
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"position": { "x": -25, "y": 0, "z": 1 }
|
||||
},
|
||||
{
|
||||
"character": "Brian",
|
||||
"position": { "x": 125, "y": 0, "z": 1 }
|
||||
}
|
||||
],
|
||||
"audio": {
|
||||
"filename": "1.1-4.mp3",
|
||||
"startTime": 1.3
|
||||
},
|
||||
"alwaysShowDialogue": true
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"character": "Sophie",
|
||||
"position": { "x": 40, "y": 0, "z": 1 },
|
||||
"startTime": 0
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"position": { "x": 25, "y": 0, "z": 1 },
|
||||
"startTime": 0.3
|
||||
},
|
||||
{
|
||||
"character": "Brian",
|
||||
"position": { "x": 75, "y": 0, "z": 1 },
|
||||
"startTime": 0.7
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 1.3,
|
||||
"finishTime": 4,
|
||||
"dialogue": {
|
||||
"text": "Oh, look who's here! Hey, Brian! How is everything?",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Brian",
|
||||
"startTime": 4.5,
|
||||
"finishTime": 6.4,
|
||||
"dialogue": {
|
||||
"text": "Sophie! Great to see you here.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 6.6,
|
||||
"finishTime": 9.8,
|
||||
"dialogue": {
|
||||
"text": "You, too. Brian, let me introduce you to Tom.",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 9.9,
|
||||
"finishTime": 12.8,
|
||||
"dialogue": {
|
||||
"text": "Tom, this is Brian. He's a web developer.",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 13,
|
||||
"finishTime": 17,
|
||||
"dialogue": {
|
||||
"text": "Brian, this is Tom. He's our new graphic designer and he is from Texas.",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Brian",
|
||||
"startTime": 17.2,
|
||||
"finishTime": 19.4,
|
||||
"dialogue": {
|
||||
"text": "It is a pleasure to meet you, Tom.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"character": "Brian",
|
||||
"startTime": 19.6,
|
||||
"finishTime": 23.6,
|
||||
"dialogue": {
|
||||
"text": "Sophie is a great workmate. She's very kind and helpful.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"startTime": 23.9,
|
||||
"finishTime": 25.4,
|
||||
"dialogue": {
|
||||
"text": "Oh, c'mon, Brian!",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"character": "Brian",
|
||||
"startTime": 25.8,
|
||||
"finishTime": 26.8,
|
||||
"dialogue": {
|
||||
"text": "But it's true.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Brian",
|
||||
"startTime": 27.1,
|
||||
"finishTime": 29.4,
|
||||
"dialogue": {
|
||||
"text": "She's the person to go to if you need help!",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sophie",
|
||||
"position": { "x": -25, "y": 0, "z": 1 },
|
||||
"startTime": 29.9
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
"position": { "x": -25, "y": 0, "z": 1 },
|
||||
"startTime": 30.2
|
||||
},
|
||||
{
|
||||
"character": "Brian",
|
||||
"position": { "x": 125, "y": 0, "z": 1 },
|
||||
"startTime": 30.6
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
+165
-2
@@ -1,9 +1,8 @@
|
||||
---
|
||||
id: 656d23d22a488510bca0e418
|
||||
title: "Dialogue 5: End of the First Day"
|
||||
title: 'Dialogue 5: End of the First Day'
|
||||
challengeType: 21
|
||||
dashedName: dialogue-5-end-of-the-first-day
|
||||
videoId: nLDychdBwUg
|
||||
---
|
||||
|
||||
# --description--
|
||||
@@ -13,3 +12,167 @@ What the video above to understand the context of the upcoming lessons.
|
||||
# --assignment--
|
||||
|
||||
Watch the video
|
||||
|
||||
# --scene--
|
||||
|
||||
```json
|
||||
{
|
||||
"setup": {
|
||||
"background": "company1-reception.png",
|
||||
"characters": [
|
||||
{
|
||||
"character": "Jake",
|
||||
"position": { "x": -25, "y": 0, "z": 1 }
|
||||
},
|
||||
{
|
||||
"character": "Sarah",
|
||||
"position": { "x": 125, "y": 0, "z": 1 }
|
||||
}
|
||||
],
|
||||
"audio": {
|
||||
"filename": "1.1-5.mp3",
|
||||
"startTime": 1.3
|
||||
},
|
||||
"alwaysShowDialogue": true
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"character": "Jake",
|
||||
"position": { "x": 30, "y": 0, "z": 1 },
|
||||
"startTime": 0
|
||||
},
|
||||
{
|
||||
"character": "Sarah",
|
||||
"position": { "x": 70, "y": 0, "z": 1 },
|
||||
"startTime": 0.7
|
||||
},
|
||||
{
|
||||
"character": "Jake",
|
||||
"startTime": 1.7,
|
||||
"finishTime": 3.5,
|
||||
"dialogue": {
|
||||
"text": "Hey. You're Sarah, right?",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Jake",
|
||||
"startTime": 3.6,
|
||||
"finishTime": 5.4,
|
||||
"dialogue": {
|
||||
"text": "I'm Jake, from Security.",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Jake",
|
||||
"startTime": 5.5,
|
||||
"finishTime": 7.3,
|
||||
"dialogue": {
|
||||
"text": "I'm here to give you your access card.",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sarah",
|
||||
"startTime": 7.7,
|
||||
"finishTime": 10,
|
||||
"dialogue": {
|
||||
"text": "Thanks, Jake. Is it contactless?",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Jake",
|
||||
"startTime": 10.2,
|
||||
"finishTime": 13.4,
|
||||
"dialogue": {
|
||||
"text": "No, it isn't. It's the good-old swipe at the door.",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Jake",
|
||||
"startTime": 13.5,
|
||||
"finishTime": 17,
|
||||
"dialogue": {
|
||||
"text": "When you hear the click, it's unlocked, and you can get in or out.",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sarah",
|
||||
"startTime": 17.9,
|
||||
"finishTime": 19.4,
|
||||
"dialogue": {
|
||||
"text": "Good to know. Thank you!",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Jake",
|
||||
"startTime": 19.6,
|
||||
"finishTime": 22,
|
||||
"dialogue": {
|
||||
"text": "Well, it's five o'clock.",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Jake",
|
||||
"startTime": 22.1,
|
||||
"finishTime": 24.2,
|
||||
"dialogue": {
|
||||
"text": "I guess this is it for your first day.",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Jake",
|
||||
"startTime": 24.5,
|
||||
"finishTime": 25.5,
|
||||
"dialogue": {
|
||||
"text": "How was it?",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sarah",
|
||||
"startTime": 25.9,
|
||||
"finishTime": 27.5,
|
||||
"dialogue": {
|
||||
"text": "Good, really good.",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Sarah",
|
||||
"startTime": 27.8,
|
||||
"finishTime": 28.8,
|
||||
"dialogue": {
|
||||
"text": "See you tomorrow, then?",
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Jake",
|
||||
"startTime": 29.0,
|
||||
"finishTime": 31.0,
|
||||
"dialogue": {
|
||||
"text": "Sure. Have a great evening. See you!",
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Jake",
|
||||
"position": { "x": -25, "y": 0, "z": 1 },
|
||||
"startTime": 31.5
|
||||
},
|
||||
{
|
||||
"character": "Sarah",
|
||||
"position": { "x": 125, "y": 0, "z": 1 },
|
||||
"startTime": 32
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -5,6 +5,11 @@ exports[`challenge schema Notify mobile team BEFORE updating snapshot 1`] = `
|
||||
Joi.objectId = require('joi-objectid')(Joi);
|
||||
|
||||
const { challengeTypes } = require('../../shared/config/challenge-types');
|
||||
const {
|
||||
availableCharacters,
|
||||
availableBackgrounds,
|
||||
availableAudios
|
||||
} = require('./scene-assets');
|
||||
|
||||
const slugRE = new RegExp('^[a-z0-9-]+$');
|
||||
const slugWithSlashRE = new RegExp('^[a-z0-9-/]+$');
|
||||
@@ -29,6 +34,55 @@ const prerequisitesJoi = Joi.object().keys({
|
||||
title: Joi.string().required()
|
||||
});
|
||||
|
||||
const positionJoi = Joi.object().keys({
|
||||
x: Joi.number().required(),
|
||||
y: Joi.number().required(),
|
||||
z: Joi.number().required()
|
||||
});
|
||||
|
||||
const setupCharacterJoi = Joi.object().keys({
|
||||
character: Joi.string()
|
||||
.valid(...availableCharacters)
|
||||
.required(),
|
||||
position: positionJoi.required(),
|
||||
opacity: Joi.number()
|
||||
});
|
||||
|
||||
const setupAudioJoi = Joi.object().keys({
|
||||
filename: Joi.string()
|
||||
.valid(...availableAudios)
|
||||
.required(),
|
||||
startTime: Joi.number().required(),
|
||||
startTimestamp: Joi.number(),
|
||||
finishTimestamp: Joi.number()
|
||||
});
|
||||
|
||||
const setupJoi = Joi.object().keys({
|
||||
background: Joi.string()
|
||||
.valid(...availableBackgrounds)
|
||||
.required(),
|
||||
characters: Joi.array().items(setupCharacterJoi).min(1).required(),
|
||||
audio: setupAudioJoi.required(),
|
||||
alwaysShowDialogue: Joi.boolean()
|
||||
});
|
||||
|
||||
const DialogueJoi = Joi.object().keys({
|
||||
text: Joi.string().required(),
|
||||
align: Joi.string()
|
||||
});
|
||||
|
||||
const commandJoi = Joi.object().keys({
|
||||
background: Joi.string().valid(...availableBackgrounds),
|
||||
character: Joi.string()
|
||||
.valid(...availableCharacters)
|
||||
.required(),
|
||||
position: positionJoi,
|
||||
opacity: Joi.number(),
|
||||
startTime: Joi.number().required(),
|
||||
finishTime: Joi.number(),
|
||||
dialogue: DialogueJoi
|
||||
});
|
||||
|
||||
const schema = Joi.object()
|
||||
.keys({
|
||||
audioPath: Joi.string(),
|
||||
@@ -92,7 +146,7 @@ const schema = Joi.object()
|
||||
}),
|
||||
// video challenges only:
|
||||
videoId: Joi.when('challengeType', {
|
||||
is: [challengeTypes.video, challengeTypes.dialogue],
|
||||
is: [challengeTypes.video],
|
||||
then: Joi.string().required()
|
||||
}),
|
||||
videoLocaleIds: Joi.when('challengeType', {
|
||||
@@ -136,6 +190,10 @@ const schema = Joi.object()
|
||||
then: Joi.array().items(Joi.string()).required(),
|
||||
otherwise: Joi.array().items(Joi.string())
|
||||
}),
|
||||
scene: Joi.object().keys({
|
||||
setup: setupJoi.required(),
|
||||
commands: Joi.array().items(commandJoi)
|
||||
}),
|
||||
solutions: Joi.array().items(Joi.array().items(fileJoi).min(1)),
|
||||
superBlock: Joi.string().regex(slugWithSlashRE),
|
||||
superOrder: Joi.number(),
|
||||
|
||||
@@ -2,6 +2,11 @@ const Joi = require('joi');
|
||||
Joi.objectId = require('joi-objectid')(Joi);
|
||||
|
||||
const { challengeTypes } = require('../../shared/config/challenge-types');
|
||||
const {
|
||||
availableCharacters,
|
||||
availableBackgrounds,
|
||||
availableAudios
|
||||
} = require('./scene-assets');
|
||||
|
||||
const slugRE = new RegExp('^[a-z0-9-]+$');
|
||||
const slugWithSlashRE = new RegExp('^[a-z0-9-/]+$');
|
||||
@@ -26,6 +31,55 @@ const prerequisitesJoi = Joi.object().keys({
|
||||
title: Joi.string().required()
|
||||
});
|
||||
|
||||
const positionJoi = Joi.object().keys({
|
||||
x: Joi.number().required(),
|
||||
y: Joi.number().required(),
|
||||
z: Joi.number().required()
|
||||
});
|
||||
|
||||
const setupCharacterJoi = Joi.object().keys({
|
||||
character: Joi.string()
|
||||
.valid(...availableCharacters)
|
||||
.required(),
|
||||
position: positionJoi.required(),
|
||||
opacity: Joi.number()
|
||||
});
|
||||
|
||||
const setupAudioJoi = Joi.object().keys({
|
||||
filename: Joi.string()
|
||||
.valid(...availableAudios)
|
||||
.required(),
|
||||
startTime: Joi.number().required(),
|
||||
startTimestamp: Joi.number(),
|
||||
finishTimestamp: Joi.number()
|
||||
});
|
||||
|
||||
const setupJoi = Joi.object().keys({
|
||||
background: Joi.string()
|
||||
.valid(...availableBackgrounds)
|
||||
.required(),
|
||||
characters: Joi.array().items(setupCharacterJoi).min(1).required(),
|
||||
audio: setupAudioJoi.required(),
|
||||
alwaysShowDialogue: Joi.boolean()
|
||||
});
|
||||
|
||||
const DialogueJoi = Joi.object().keys({
|
||||
text: Joi.string().required(),
|
||||
align: Joi.string()
|
||||
});
|
||||
|
||||
const commandJoi = Joi.object().keys({
|
||||
background: Joi.string().valid(...availableBackgrounds),
|
||||
character: Joi.string()
|
||||
.valid(...availableCharacters)
|
||||
.required(),
|
||||
position: positionJoi,
|
||||
opacity: Joi.number(),
|
||||
startTime: Joi.number().required(),
|
||||
finishTime: Joi.number(),
|
||||
dialogue: DialogueJoi
|
||||
});
|
||||
|
||||
const schema = Joi.object()
|
||||
.keys({
|
||||
audioPath: Joi.string(),
|
||||
@@ -89,7 +143,7 @@ const schema = Joi.object()
|
||||
}),
|
||||
// video challenges only:
|
||||
videoId: Joi.when('challengeType', {
|
||||
is: [challengeTypes.video, challengeTypes.dialogue],
|
||||
is: [challengeTypes.video],
|
||||
then: Joi.string().required()
|
||||
}),
|
||||
videoLocaleIds: Joi.when('challengeType', {
|
||||
@@ -133,6 +187,10 @@ const schema = Joi.object()
|
||||
then: Joi.array().items(Joi.string()).required(),
|
||||
otherwise: Joi.array().items(Joi.string())
|
||||
}),
|
||||
scene: Joi.object().keys({
|
||||
setup: setupJoi.required(),
|
||||
commands: Joi.array().items(commandJoi)
|
||||
}),
|
||||
solutions: Joi.array().items(Joi.array().items(fileJoi).min(1)),
|
||||
superBlock: Joi.string().regex(slugWithSlashRE),
|
||||
superOrder: Joi.number(),
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
const availableCharacters = [
|
||||
'Alice',
|
||||
'Anna',
|
||||
'Bob',
|
||||
'Brian',
|
||||
'David',
|
||||
'Jake',
|
||||
'James',
|
||||
'Linda',
|
||||
'Lisa',
|
||||
'Maria',
|
||||
'Sarah',
|
||||
'Sophie',
|
||||
'Tom'
|
||||
];
|
||||
|
||||
const availableBackgrounds = [
|
||||
'bedroom-empty.png',
|
||||
'cafe.png',
|
||||
'chaos.png',
|
||||
'classroom.png',
|
||||
'company1-boardroom.png',
|
||||
'company1-breakroom.png',
|
||||
'company1-center.png',
|
||||
'company1-dining.png',
|
||||
'company1-lydia-cubicle.png',
|
||||
'company1-parking.png',
|
||||
'company1-reception.png',
|
||||
'company1-roof.png',
|
||||
'company2-boardroom.png',
|
||||
'company2-breakroom.png',
|
||||
'company2-center.png',
|
||||
'company2-dining.png',
|
||||
'company2-lydia-cubicle.png',
|
||||
'company2-parking.png',
|
||||
'company2-reception.png',
|
||||
'company2-roof.png',
|
||||
'company3-boardmeeting.png',
|
||||
'company3-breakroom.png',
|
||||
'company3-center.png',
|
||||
'company3-dining.png',
|
||||
'company3-lydia-cubicle.png',
|
||||
'company3-parking.png',
|
||||
'company3-reception.png',
|
||||
'company3-roof.png',
|
||||
'cubicle.png',
|
||||
'desk.png',
|
||||
'farm.png',
|
||||
'hacker-space-cafe.png',
|
||||
'hacker-space.png',
|
||||
'hall-audience.png',
|
||||
'hall.png',
|
||||
'interview-room1.png',
|
||||
'interview-room2.png',
|
||||
'interview-room3.png',
|
||||
'kid-home.png',
|
||||
'kitchen.png',
|
||||
'laptop-screen.png',
|
||||
'living-room.png',
|
||||
'office-cafe.png',
|
||||
'office.png',
|
||||
'park1.png',
|
||||
'park2.png',
|
||||
'park3.png',
|
||||
'park4.png',
|
||||
'pong-field.png',
|
||||
'tunnel.png'
|
||||
];
|
||||
|
||||
const availableAudios = [
|
||||
'1.1-1.mp3',
|
||||
'1.1-2.mp3',
|
||||
'1.1-3.mp3',
|
||||
'1.1-4.mp3',
|
||||
'1.1-5.mp3',
|
||||
'1.2-1.mp3',
|
||||
'1.2-2.mp3',
|
||||
'1.2-3.mp3',
|
||||
'1.2-4.mp3',
|
||||
'1.2-5.mp3',
|
||||
'1.3-1.mp3',
|
||||
'1.3-2.mp3',
|
||||
'1.3-3.mp3',
|
||||
'1.3-4.mp3',
|
||||
'1.3-5.mp3'
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
availableCharacters,
|
||||
availableBackgrounds,
|
||||
availableAudios
|
||||
};
|
||||
@@ -27,7 +27,6 @@ Before you work on the curriculum, you would need to set up some tooling to help
|
||||
|
||||
[](https://gitpod.io/#https://github.com/freeCodeCamp/freeCodeCamp)
|
||||
|
||||
|
||||
### How to work on practice projects
|
||||
|
||||
The practice projects have some additional tooling to help create new projects and steps. To read more, see [these docs](how-to-work-on-practice-projects.md)
|
||||
@@ -200,6 +199,104 @@ Solution for the second blank. Example:
|
||||
`right`
|
||||
|
||||
If no feedback is here, a generic "wrong answer" message will be shown.
|
||||
|
||||
# --scene--
|
||||
|
||||
```json
|
||||
// # --scene-- can only consist of a single json object
|
||||
{
|
||||
// Setup the scene. Properties not marked optional are required.
|
||||
"setup": {
|
||||
// Background file to start the scene. A list of scene asset filenames can be found here: https://github.com/freeCodeCamp/cdn/pull/233/files
|
||||
"background": "company2-center.png",
|
||||
// Array of all characters that will appear in the scene
|
||||
"characters": [
|
||||
{
|
||||
// Name of character. See list of available characters in scene-assets.tsx
|
||||
"character": "Maria",
|
||||
// Where to start the character. Maria will start off screen to the left
|
||||
"position": { "x": -25, "y": 0, "z": 1 }
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
// Tom will start 70% from the left of the screen and 1.5 times regular size
|
||||
"position": { "x": 70, "y": 0, "z": 1.5 },
|
||||
// Optional, defaults to 1. Tom will start invisible
|
||||
"opacity": 0
|
||||
}
|
||||
],
|
||||
"audio": {
|
||||
// Audio filename
|
||||
"filename": "1.1-1.mp3",
|
||||
// Seconds after the scene starts before the audio starts playing
|
||||
"startTime": 1.3,
|
||||
// Optional. Timestamp of the audio file where it starts playing from.
|
||||
"startTimestamp": 0,
|
||||
// Optional. Timestamp of the audio file where is stops playing. If these two aren't used, the whole audio file will play.
|
||||
"finishTimestamp": 8.4
|
||||
},
|
||||
// Optional, defaults to false. Use this for the long dialogues. It stops the accessibility icon from showing which gives campers the option to show or hide the dialogue text
|
||||
"alwaysShowDialogue": true
|
||||
},
|
||||
// Array of commands that make up the scene
|
||||
"commands": [
|
||||
{
|
||||
// Character that will have an action for this command
|
||||
"character": "Maria",
|
||||
// Optional, defaults to previous value. Maria will move to 25% from the left of the screen. The movement takes 0.5 seconds
|
||||
"position": { "x": 25, "y": 0, "z": 1 },
|
||||
// When the command will start. Zero seconds after the camper presses play
|
||||
"startTime": 0
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
// Optional, defaults to previous value. Tom will fade into view. The transition take 0.5 seconds. Movement and Opacity transitions take 0.5 seconds
|
||||
"opacity": 1,
|
||||
// Tom will fade into view 0.5 seconds into the scene (immediately after Maria finishes moving on screen)
|
||||
"startTime": 0.5
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
// When the command starts: Maria will start saying this line 1.3 seconds into the scene. Note that this is the same time as the audio.startTime above. It doesn't have to match that (maybe there's a pause at the begninning of the audio or something)
|
||||
"startTime": 1.3,
|
||||
// The character will stop moving their mouth at the finishTime
|
||||
"finishTime": 4.95,
|
||||
"dialogue": {
|
||||
// Text that will appear if the dialogue is visible
|
||||
"text": "Hello! You're the new graphic designer, right? I'm Maria, the team lead.",
|
||||
// Where the dialogue text will be aligned. Can be 'left', 'center', or 'right'
|
||||
"align": "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
// background will change to this at 5.4 seconds into the scene
|
||||
"background": "company2-breakroom.png",
|
||||
"character": "Tom",
|
||||
"startTime": 5.4,
|
||||
"finishTime": 9.4,
|
||||
"dialogue": {
|
||||
"text": "Hi, that's right! I'm Tom McKenzie. It's a pleasure to meet you.",
|
||||
// Tom's text will be aligned to the right since he is on the right side of the screen
|
||||
"align": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Tom",
|
||||
// Tom will fade to 0 opacity
|
||||
"opacity": 0,
|
||||
// I like to move characters off screen or fade them 0.5 second after the last talking command
|
||||
"startTime": 9.9
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
// Maria will slide back off the screen to the left
|
||||
"position": { "x": -25, "y": 0, "z": 1 },
|
||||
// The animation will stop playing 0.5 seconds after the 'finishTime' of the last command - or 0.5 seconds after 'startTime' if 'finishTime' isn't there.
|
||||
"startTime": 10.4
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# --description--
|
||||
|
||||
This challenge has a scene.
|
||||
|
||||
# --scene--
|
||||
|
||||
```json
|
||||
{
|
||||
"setup": {
|
||||
"background": "company2-center.png",
|
||||
"characters": [
|
||||
{
|
||||
"character": "Maria",
|
||||
"position": { "x": 50, "y": 0, "z": 1.5 },
|
||||
"opacity": 0
|
||||
}
|
||||
],
|
||||
"audio": {
|
||||
"filename": "1.1-1.mp3",
|
||||
"startTime": 1,
|
||||
"startTimestamp": 2.6,
|
||||
"finishTimestamp": 4
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"character": "Maria",
|
||||
"opacity": 1,
|
||||
"startTime": 0
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"startTime": 0.7,
|
||||
"finishTime": 2.4,
|
||||
"dialogue": {
|
||||
"text": "I'm Maria, the team lead.",
|
||||
"align": "center"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"opacity": 0,
|
||||
"startTime": 3.4
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -18,6 +18,60 @@ a container directive
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`challenge parser it should parse md with a scene 1`] = `
|
||||
{
|
||||
"assignments": [],
|
||||
"description": "<section id="description">
|
||||
<p>This challenge has a scene.</p>
|
||||
</section>",
|
||||
"scene": {
|
||||
"commands": [
|
||||
{
|
||||
"character": "Maria",
|
||||
"opacity": 1,
|
||||
"startTime": 0,
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"dialogue": {
|
||||
"align": "center",
|
||||
"text": "I'm Maria, the team lead.",
|
||||
},
|
||||
"finishTime": 2.4,
|
||||
"startTime": 0.7,
|
||||
},
|
||||
{
|
||||
"character": "Maria",
|
||||
"opacity": 0,
|
||||
"startTime": 3.4,
|
||||
},
|
||||
],
|
||||
"setup": {
|
||||
"audio": {
|
||||
"filename": "1.1-1.mp3",
|
||||
"finishTimestamp": 4,
|
||||
"startTime": 1,
|
||||
"startTimestamp": 2.6,
|
||||
},
|
||||
"background": "company2-center.png",
|
||||
"characters": [
|
||||
{
|
||||
"character": "Maria",
|
||||
"opacity": 0,
|
||||
"position": {
|
||||
"x": 50,
|
||||
"y": 0,
|
||||
"z": 1.5,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"solutions": [],
|
||||
"tests": [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`challenge parser it should parse video questions 1`] = `
|
||||
{
|
||||
"assignments": [],
|
||||
|
||||
@@ -58,4 +58,11 @@ describe('challenge parser', () => {
|
||||
);
|
||||
expect(parsed).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('it should parse md with a scene', async () => {
|
||||
const parsed = await parseMD(
|
||||
path.resolve(__dirname, '__fixtures__/scene.md')
|
||||
);
|
||||
expect(parsed).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ const addAssignment = require('./plugins/add-assignment');
|
||||
const replaceImports = require('./plugins/replace-imports');
|
||||
const restoreDirectives = require('./plugins/restore-directives');
|
||||
const tableAndStrikeThrough = require('./plugins/table-and-strikethrough');
|
||||
const addScene = require('./plugins/add-scene');
|
||||
|
||||
// by convention, anything that adds to file.data has the name add<name>.
|
||||
const processor = unified()
|
||||
@@ -48,6 +49,7 @@ const processor = unified()
|
||||
.use(addFillInTheBlank)
|
||||
.use(addVideoQuestion)
|
||||
.use(addAssignment)
|
||||
.use(addScene)
|
||||
.use(addTests)
|
||||
.use(addText, ['description', 'instructions', 'notes']);
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
const getAllBetween = require('./utils/between-headings');
|
||||
|
||||
function plugin() {
|
||||
return transformer;
|
||||
|
||||
function transformer(tree, file) {
|
||||
const sceneNodes = getAllBetween(tree, '--scene--');
|
||||
|
||||
if (sceneNodes.length > 0) {
|
||||
if (sceneNodes.length !== 1) {
|
||||
throw Error('You can only have one item in a scene, a JSON array.');
|
||||
}
|
||||
|
||||
if (sceneNodes[0].type !== 'code' || sceneNodes[0].lang !== 'json') {
|
||||
throw Error('A scene must have a ```json code block');
|
||||
}
|
||||
|
||||
// throws if we can't parse it.
|
||||
const sceneJson = JSON.parse(sceneNodes[0].value);
|
||||
file.data.scene = sceneJson;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = plugin;
|
||||
Reference in New Issue
Block a user