diff --git a/client/gatsby-node.js b/client/gatsby-node.js index 509bb00cb59..0099d737c87 100644 --- a/client/gatsby-node.js +++ b/client/gatsby-node.js @@ -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); }; diff --git a/client/src/assets/icons/accessibility.tsx b/client/src/assets/icons/accessibility.tsx new file mode 100644 index 00000000000..f9c06cc1d0c --- /dev/null +++ b/client/src/assets/icons/accessibility.tsx @@ -0,0 +1,63 @@ +import React from 'react'; + +function AccessibilityIcon( + props: JSX.IntrinsicAttributes & React.SVGProps +): JSX.Element { + return ( + <> + + + + + + + + + ); +} + +AccessibilityIcon.displayName = 'AccessibilityIcon'; + +export default AccessibilityIcon; diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts index 07347c21a8b..be8441b2f37 100644 --- a/client/src/redux/prop-types.ts +++ b/client/src/redux/prop-types.ts @@ -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; }; diff --git a/client/src/templates/Challenges/components/scene/character.css b/client/src/templates/Challenges/components/scene/character.css new file mode 100644 index 00000000000..b725d82b813 --- /dev/null +++ b/client/src/templates/Challenges/components/scene/character.css @@ -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; +} diff --git a/client/src/templates/Challenges/components/scene/character.tsx b/client/src/templates/Challenges/components/scene/character.tsx new file mode 100644 index 00000000000..92a2ae85fcd --- /dev/null +++ b/client/src/templates/Challenges/components/scene/character.tsx @@ -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 ( +
+ Character Body + Character Brows + Character Eyes + Character Mouth + {glasses && ( + Character Glasses + )} +
+ ); +} + +Character.displayName = 'Character'; + +export default Character; diff --git a/client/src/templates/Challenges/components/scene/scene-assets.tsx b/client/src/templates/Challenges/components/scene/scene-assets.tsx new file mode 100644 index 00000000000..edf4a31c0c2 --- /dev/null +++ b/client/src/templates/Challenges/components/scene/scene-assets.tsx @@ -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` + } +}; diff --git a/client/src/templates/Challenges/components/scene/scene.css b/client/src/templates/Challenges/components/scene/scene.css new file mode 100644 index 00000000000..5cbf2a469a7 --- /dev/null +++ b/client/src/templates/Challenges/components/scene/scene.css @@ -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; +} diff --git a/client/src/templates/Challenges/components/scene/scene.tsx b/client/src/templates/Challenges/components/scene/scene.tsx new file mode 100644 index 00000000000..7a8ae710aa4 --- /dev/null +++ b/client/src/templates/Challenges/components/scene/scene.tsx @@ -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( + 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 ( + +
+ {!sceneIsReady ? ( + + ) : ( + <> + {characters.map( + ( + { character, position = {}, opacity = 1, isTalking = false }, + i + ) => { + return ( + + ); + } + )} + + {showDialogue && (alwaysShowDialogue || accessibilityOn) && ( +
+
{dialogue.label}
+
{dialogue.text}
+
+ )} + + {!isPlaying && ( +
+ + + {!alwaysShowDialogue && ( + + )} +
+ )} + + )} +
+ + ); +} + +Scene.displayName = 'Scene'; + +export default Scene; diff --git a/client/src/templates/Challenges/dialogue/show.tsx b/client/src/templates/Challenges/dialogue/show.tsx index 599270745e4..da8c3dfb37e 100644 --- a/client/src/templates/Challenges/dialogue/show.tsx +++ b/client/src/templates/Challenges/dialogue/show.tsx @@ -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 { description, superBlock, block, - videoId, fields: { blockName }, - assignments + assignments, + scene } } }, @@ -210,29 +209,16 @@ class ShowDialogue extends Component { /> - {videoId && ( - - -
- {!this.state.videoIsLoaded ? ( -
- -
- ) : null} - -
- - )}

{title}

+ + + + +

{t('learn.assignments')}

@@ -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 + } + } + } } } } diff --git a/client/src/templates/Challenges/fill-in-the-blank/show.tsx b/client/src/templates/Challenges/fill-in-the-blank/show.tsx index 239c0c00180..2b839114d1d 100644 --- a/client/src/templates/Challenges/fill-in-the-blank/show.tsx +++ b/client/src/templates/Challenges/fill-in-the-blank/show.tsx @@ -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< )} + + + {scene && } + + @@ -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 } diff --git a/client/src/templates/Challenges/odin/show.tsx b/client/src/templates/Challenges/odin/show.tsx index e5982fd3cc2..3b1de183eb4 100644 --- a/client/src/templates/Challenges/odin/show.tsx +++ b/client/src/templates/Challenges/odin/show.tsx @@ -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 { fields: { blockName }, question: { text, answers, solution }, assignments, - audioPath + audioPath, + scene } } }, @@ -292,6 +294,11 @@ class ShowOdin extends Component { )} + + + {scene && } + + {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 diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/651dd3e06ffb500e3f2ce478.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/651dd3e06ffb500e3f2ce478.md index 0c62bf1f8b6..493853f0f95 100644 --- a/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/651dd3e06ffb500e3f2ce478.md +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/651dd3e06ffb500e3f2ce478.md @@ -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 + } + ] +} +``` diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/651dd5296ffb500e3f2ce479.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/651dd5296ffb500e3f2ce479.md index 61ffe540d0c..21e8350b483 100644 --- a/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/651dd5296ffb500e3f2ce479.md +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/651dd5296ffb500e3f2ce479.md @@ -3,30 +3,73 @@ id: 651dd5296ffb500e3f2ce479 title: Task 1 challengeType: 22 dashedName: task-1 -audioPath: 1.1-1.mp3#t=1.6,3.3 --- # --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 + } + ] +} +``` diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/651dd5386ffb500e3f2ce47a.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/651dd5386ffb500e3f2ce47a.md index 8cd74dac4c0..b90d0fc5666 100644 --- a/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/651dd5386ffb500e3f2ce47a.md +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/651dd5386ffb500e3f2ce47a.md @@ -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 ---