Creative Visual Effects

Deep-Sea Interactive Particle Field

Complete an interactive particle field in a deep-sea scene: includes real content, actionable controls, clear status feedback, and responsive layouts.

Creative Visual EffectsInteractive Particle FieldHTMLCanvasJavaScript
With JavaScript enabled, you can edit, run, and save this example. The fixed source is as follows:
<!doctype html>
<html lang="en" dir="ltr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Deep-Sea Interactive Particle Field</title>
  <style>
    :root { --ink:#18332f; --paper:#f4efe4; --accent:#d45f3e; --secondary:#8fc4ad; --line:color-mix(in srgb, var(--ink) 18%, transparent); --shadow:0 24px 60px color-mix(in srgb, var(--ink) 16%, transparent); color-scheme:light; }
    * { box-sizing:border-box; }
    html { min-width:320px; background:var(--paper); }
    body { min-height:100vh; margin:0; overflow-x:hidden; color:var(--ink); font-family:"Songti SC","Noto Serif SC",Georgia,serif; background:linear-gradient(90deg,transparent 49.7%,color-mix(in srgb,var(--ink) 7%,transparent) 50%,transparent 50.3%),var(--paper); }
    button,input,select,textarea { color:inherit; font:inherit; }
    button,[role="button"],input,select,textarea { accent-color:var(--accent); }
    button { cursor:pointer; }
    button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[tabindex]:focus-visible { outline:3px solid var(--accent); outline-offset:3px; }
    [hidden] { display:none !important; }
    @media (max-width:700px) { .mast h1 { line-height:1.08 !important; letter-spacing:-.035em !important; overflow-wrap:anywhere; } }
    @media (prefers-reduced-motion:reduce) { *,*::before,*::after { scroll-behavior:auto !important; animation-duration:.01ms !important; animation-iteration-count:1 !important; transition-duration:.01ms !important; } }
    .page{width:min(1180px,calc(100% - 40px));margin:auto;padding:7vw 0}.mast{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(180px,.4fr);gap:5vw;align-items:end;margin-bottom:38px}.mast h1{font-family:"Songti SC",Georgia,serif;font-weight:800;font-size:clamp(42px,9vw,92px);max-width:920px;margin:0;line-height:.94}.eyebrow{display:block;margin-bottom:14px;color:var(--accent);font:800 11px/1 ui-monospace,monospace;letter-spacing:.24em}.intro{max-width:58ch;line-height:1.9}.scenario{align-self:start;padding-top:10px;border-top:1px solid var(--ink);font-style:italic}.demo{border-radius:0;border-width:2px;box-shadow:14px 14px 0 color-mix(in srgb,var(--ink) 18%,transparent);position:relative;overflow:hidden;min-height:420px;padding:clamp(22px,4vw,48px);background:var(--paper)}@media(max-width:700px){.page{width:calc(100% - 22px);padding:26px 0}.mast{grid-template-columns:1fr}.demo{min-height:340px;padding:18px}}
    .panel{padding:22px;border:1px solid var(--line);background:color-mix(in srgb,#fff 58%,transparent)}.stack{display:grid;gap:15px}.row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}.action{min-height:44px;padding:10px 17px;border:1px solid var(--ink);border-radius:999px;background:transparent}.action.primary,.action.is-active{color:var(--paper);background:var(--accent)}.muted{opacity:.68}.status{min-height:24px;margin-top:15px;color:var(--accent);font-weight:800}.metric{font-size:clamp(30px,6vw,58px);font-weight:900}.meter{height:9px;overflow:hidden;border-radius:999px;background:color-mix(in srgb,var(--ink) 12%,transparent)}.meter>i{display:block;height:100%;background:var(--accent)}.chip{padding:6px 9px;border:1px solid var(--line);border-radius:999px;font-size:12px}
    .composition-split{display:grid;grid-template-columns:150px minmax(0,1fr);gap:18px;align-items:start}.split-note{position:sticky;top:20px;display:grid;gap:14px;padding:18px 12px;border-block:3px double var(--accent);font-size:13px;line-height:1.65}.split-note b{font:900 12px/1 ui-monospace,monospace;letter-spacing:.12em}.split-note span{writing-mode:vertical-rl;min-height:120px;color:color-mix(in srgb,var(--ink) 60%,transparent)}.composition-split .demo{min-width:0}@media(max-width:900px){.composition-split{grid-template-columns:1fr}.split-note{position:static;grid-template-columns:auto 1fr}.split-note span{writing-mode:horizontal-tb;min-height:0}}
    .particle-field-lab{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(230px,.6fr);gap:16px}.particle-field-lab canvas{width:100%;height:460px;border:1px solid var(--line);border-radius:22px;background:#07131f;touch-action:none}.particle-controls{display:grid;align-content:center;gap:17px}.particle-controls label{display:grid;grid-template-columns:1fr auto;gap:8px;font-weight:800}.particle-controls input{grid-column:1/-1;width:100%}@media(max-width:760px){.particle-field-lab{grid-template-columns:1fr}.particle-field-lab canvas{height:330px}}.workflow-dispatch{display:grid;grid-template-columns:minmax(210px,.48fr) minmax(0,1.52fr);gap:18px}.dispatch-console{display:grid;align-content:start;gap:16px;padding:22px;border:1px solid var(--line);border-radius:18px;background:color-mix(in srgb,var(--secondary) 12%,var(--paper))}.dispatch-console h2{margin:.2em 0}.dispatch-console form,.dispatch-console label{display:grid;gap:7px}.dispatch-console input,.dispatch-console select{min-width:0;width:100%;padding:10px;border:1px solid var(--line);border-radius:8px;background:var(--paper)}.dispatch-console fieldset{display:flex;gap:12px;border:1px solid var(--line);border-radius:10px}.dispatch-console fieldset label{display:flex;align-items:center}.dispatch-workbench{--dispatch-load:.22;min-width:0;padding:14px;border-left:8px solid color-mix(in srgb,var(--accent) calc(var(--dispatch-load)*100%),var(--line));background:linear-gradient(90deg,color-mix(in srgb,var(--accent) 8%,transparent),transparent 28%)}.dispatch-workbench[data-dispatch-priority="urgent"]{box-shadow:inset 0 0 0 2px var(--accent)}@media(max-width:900px){.workflow-dispatch{grid-template-columns:1fr}.dispatch-console{order:2}}
  </style>
</head>
<body data-aesthetic="editorial" data-composition="split-note">
  <main class="page">
    <header class="mast"><div><span class="eyebrow">editorial / Interactive Particle Field / INTERMEDIATE</span><h1>Deep-Sea Interactive Particle Field</h1><p class="intro">Complete an interactive particle field in a deep-sea scene: includes real content, actionable controls, clear status feedback, and responsive layouts.</p></div><span class="scenario">Deep Sea</span></header>
    <div class="composition-split"><aside class="split-note"><b>CASE 02</b><span>Master the semantic structure, responsive composition, keyboard interaction, and state synchronization of interactive particle fields.</span></aside><section class="demo" aria-label="Deep Sea Interactive Particle Field Interaction Area"><div class="workflow-dispatch"><aside class="dispatch-console"><span class="eyebrow">Deep Sea DISPATCH</span><h2>Submarine mission control</h2><form id="workflowDispatchForm"><label>Task Type<select id="workflowDispatchKind"><option>Sonar Scan</option><option>Hot Spring Sampling</option><option>Return Beacon</option></select></label><label>Subsea Depth<input id="workflowDispatchAmount" type="number" min="1" max="9999" value="18"><span>m</span></label><fieldset><legend>Processing priority</legend><label><input type="radio" name="workflowPriority" value="normal" checked>Regular</label><label><input type="radio" name="workflowPriority" value="urgent">Urgent</label></fieldset><button class="action primary" type="submit">Dive in</button></form><label>Current task<select id="workflowDispatchActive" aria-label="Switch current task"><option value="1">Sonar Scan</option><option value="2">Hot Spring Sampling</option></select></label><output id="workflowDispatchSummary">Waiting for Dispatch</output></aside><section id="workflowCore" class="workflow-core dispatch-workbench" data-dispatch-priority="normal"><!-- 11DZ_CORE_START:creative-effects-particle-field --><div class="particle-field-lab"><canvas id="particleCanvas" width="960" height="480" tabindex="0" aria-label="Deep-Sea Interactive Particle Field; move the pointer or use the arrow keys to attract particles" aria-describedby="particleHelp">Your browser does not support Canvas.</canvas><aside class="particle-controls panel"><span class="eyebrow">PARTICLE FIELD</span><h2>Interactive Particle Field</h2><p id="particleHelp" class="muted">Move the pointer on the canvas, or focus the canvas and use the arrow keys to move the attractor point.</p><label>Particle Count <input id="particleAmount" type="range" min="24" max="110" value="64" data-core-bridge="creative-effects-particle-field"><output id="particleAmountValue">64</output></label><label>Attraction Intensity <input id="particlePull" type="range" min="0" max="100" value="45"><output id="particlePullValue">45%</output></label><div class="row"><button id="particleReset" class="action primary" type="button">Reset particles</button><button id="particlePause" class="action" type="button" aria-pressed="false">Pause</button></div></aside></div><!-- 11DZ_CORE_END --></section></div><p id="status" class="status" role="status" aria-live="polite"></p></section></div>
  </main>
  <script>
    'use strict';
    const statusOutput = document.querySelector('#status');
    const announce = message => { statusOutput.textContent = message; };
    /* 11DZ_CORE_SCRIPT_START:creative-effects-particle-field */
const particleCanvas=document.querySelector('#particleCanvas');const particleContext=particleCanvas.getContext('2d');const particleAmount=document.querySelector('#particleAmount');const particlePull=document.querySelector('#particlePull');const particleReduced=matchMedia('(prefers-reduced-motion: reduce)').matches;let particlePaused=particleReduced;let particleList=[];let particleTarget={x:0,y:0,active:false};let particleFrame=0;function sizeParticleCanvas(){const rect=particleCanvas.getBoundingClientRect();const ratio=Math.min(2,devicePixelRatio||1);particleCanvas.width=Math.max(320,Math.round(rect.width*ratio));particleCanvas.height=Math.max(260,Math.round(rect.height*ratio));particleContext.setTransform(ratio,0,0,ratio,0,0);if(!particleTarget.active){particleTarget.x=rect.width/2;particleTarget.y=rect.height/2;}}function resetParticles(){cancelAnimationFrame(particleFrame);const rect=particleCanvas.getBoundingClientRect();particleList=Array.from({length:Number(particleAmount.value)},(_,index)=>({x:(index*83%997)/997*rect.width,y:(index*47%509)/509*rect.height,vx:Math.sin(index*2.17)*.45,vy:Math.cos(index*1.73)*.45,r:1.5+index%4}));document.querySelector('#particleAmountValue').textContent=particleAmount.value;drawParticleField();announce('Particle field reset, total '+particleList.length+' particle');}function drawParticleField(){const rect=particleCanvas.getBoundingClientRect();particleContext.clearRect(0,0,rect.width,rect.height);particleContext.fillStyle='#07131f';particleContext.fillRect(0,0,rect.width,rect.height);const force=Number(particlePull.value)/17000;particleList.forEach((particle,index)=>{if(!particlePaused){if(particleTarget.active){const dx=particleTarget.x-particle.x;const dy=particleTarget.y-particle.y;const distance=Math.max(30,Math.hypot(dx,dy));particle.vx+=dx/distance*force*distance;particle.vy+=dy/distance*force*distance;}particle.vx*=.992;particle.vy*=.992;particle.x+=particle.vx;particle.y+=particle.vy;if(particle.x<0||particle.x>rect.width)particle.vx*=-1;if(particle.y<0||particle.y>rect.height)particle.vy*=-1;particle.x=Math.max(0,Math.min(rect.width,particle.x));particle.y=Math.max(0,Math.min(rect.height,particle.y));}particleContext.beginPath();particleContext.fillStyle=index%3?'#67e8f9':'#ff8a65';particleContext.arc(particle.x,particle.y,particle.r,0,Math.PI*2);particleContext.fill();particleList.slice(index+1,index+7).forEach(other=>{const distance=Math.hypot(other.x-particle.x,other.y-particle.y);if(distance<72){particleContext.beginPath();particleContext.strokeStyle='rgba(103,232,249,'+(1-distance/72)*.22+')';particleContext.moveTo(particle.x,particle.y);particleContext.lineTo(other.x,other.y);particleContext.stroke();}});});if(!particlePaused)particleFrame=requestAnimationFrame(drawParticleField);}function moveParticleTarget(clientX,clientY){const rect=particleCanvas.getBoundingClientRect();particleTarget={x:clientX-rect.left,y:clientY-rect.top,active:true};}particleCanvas.addEventListener('pointermove',event=>moveParticleTarget(event.clientX,event.clientY));particleCanvas.addEventListener('pointerleave',()=>{particleTarget.active=false;});particleCanvas.addEventListener('keydown',event=>{if(!['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(event.key))return;event.preventDefault();particleTarget.active=true;particleTarget.x+=event.key==='ArrowRight'?18:event.key==='ArrowLeft'?-18:0;particleTarget.y+=event.key==='ArrowDown'?18:event.key==='ArrowUp'?-18:0;particleTarget.x=Math.max(0,Math.min(particleCanvas.clientWidth,particleTarget.x));particleTarget.y=Math.max(0,Math.min(particleCanvas.clientHeight,particleTarget.y));drawParticleField();});particleAmount.addEventListener('input',resetParticles);particlePull.addEventListener('input',()=>{document.querySelector('#particlePullValue').textContent=particlePull.value+'%';announce('Particle attraction intensity '+particlePull.value+'%');});document.querySelector('#particleReset').addEventListener('click',resetParticles);document.querySelector('#particlePause').addEventListener('click',event=>{particlePaused=!particlePaused;event.currentTarget.textContent=particlePaused?'Continue':'Pause';event.currentTarget.setAttribute('aria-pressed',String(particlePaused));cancelAnimationFrame(particleFrame);drawParticleField();announce(particlePaused?'Particle field paused':'Particle field keeps moving');});addEventListener('resize',()=>{sizeParticleCanvas();resetParticles();},{passive:true});sizeParticleCanvas();resetParticles();
/* 11DZ_CORE_SCRIPT_END */
const workflowBridgeSpec={"name":"creative-effects-particle-field","selector":"[data-core-bridge=\"creative-effects-particle-field\"]","mode":"range","key":"ArrowRight"};const workflowBridgeCore=document.querySelector('#workflowCore');const workflowBridgeControl=workflowBridgeCore.querySelector(workflowBridgeSpec.selector);if(!workflowBridgeControl)throw new Error('Missing core bridge: '+workflowBridgeSpec.name);const workflowBridgeControlledId=workflowBridgeControl.getAttribute('aria-controls');const workflowBridgeControlled=workflowBridgeControlledId?document.getElementById(workflowBridgeControlledId):null;workflowBridgeCore.dataset.workflowBridge=workflowBridgeSpec.name;workflowBridgeCore.dataset.workflowVersion='0';function workflowBridgeSerialize(){const visualNode=workflowBridgeCore.firstElementChild||workflowBridgeCore;const visual=getComputedStyle(visualNode);const outputs=[...workflowBridgeCore.querySelectorAll('output,[role="status"],[aria-live],meter,progress')].map(node=>node.textContent.trim()+'|'+node.getAttribute('aria-valuenow')+'|'+node.getAttribute('value'));const canvas=[...workflowBridgeCore.querySelectorAll('canvas')].map(node=>{try{return node.toDataURL().slice(-160);}catch{return 'canvas-unavailable';}});return JSON.stringify({html:workflowBridgeCore.innerHTML,status:statusOutput.textContent,outputs,canvas,visual:[visual.display,visual.visibility,visual.opacity,visual.transform,visual.filter,visual.backgroundColor,visual.width,visual.height],scroll:workflowBridgeSpec.mode==='scroll'?workflowBridgeControl.scrollTop:null});}let workflowBridgeLastEffect=workflowBridgeSerialize();function workflowBridgeSummary(){const label=(workflowBridgeControl.getAttribute('aria-label')||workflowBridgeControl.textContent||workflowBridgeControl.value||workflowBridgeSpec.name).trim().replace(/\s+/g,' ').slice(0,72);return label+' · Core Version '+workflowBridgeCore.dataset.workflowVersion;}function workflowBridgeCommit(){const effect=workflowBridgeSerialize();if(effect===workflowBridgeLastEffect)return {changed:false,summary:workflowBridgeSummary()};workflowBridgeLastEffect=effect;workflowBridgeCore.dataset.workflowVersion=String(Number(workflowBridgeCore.dataset.workflowVersion||0)+1);return {changed:true,summary:workflowBridgeSummary()};}function workflowBridgeScheduleCommit(){queueMicrotask(workflowBridgeCommit);setTimeout(workflowBridgeCommit,0);}workflowBridgeControl.addEventListener("input",workflowBridgeScheduleCommit);workflowBridgeControl.addEventListener('change',workflowBridgeScheduleCommit);const workflowBridgeStateTargets=new Set(workflowBridgeCore.querySelectorAll('dialog'));if(workflowBridgeControlled&&workflowBridgeCore.contains(workflowBridgeControlled))workflowBridgeStateTargets.add(workflowBridgeControlled);workflowBridgeStateTargets.forEach(target=>['close','cancel','toggle'].forEach(eventName=>target.addEventListener(eventName,workflowBridgeScheduleCommit)));function workflowBridgeActivate(){workflowBridgeControl.focus();const step=Number(workflowBridgeControl.step)||1;const min=workflowBridgeControl.min===''?-Infinity:Number(workflowBridgeControl.min);const max=workflowBridgeControl.max===''?Infinity:Number(workflowBridgeControl.max);let next=Math.min(max,Math.max(min,(Number(workflowBridgeControl.value)||0)+step));if(next===Number(workflowBridgeControl.value))next=Math.max(min,Number(workflowBridgeControl.value)-step);workflowBridgeControl.value=String(next);workflowBridgeControl.dispatchEvent(new Event('input',{bubbles:true}));workflowBridgeControl.dispatchEvent(new Event('change',{bubbles:true}));const result=workflowBridgeCommit();setTimeout(workflowBridgeCommit,0);return result;}function workflowBridgeReset(){workflowBridgeCore.dataset.workflowVersion='0';workflowBridgeLastEffect=workflowBridgeSerialize();}
const workflowDispatchCore=document.querySelector('#workflowCore');const workflowDispatchForm=document.querySelector('#workflowDispatchForm');const workflowDispatchSelect=document.querySelector('#workflowDispatchActive');const workflowDispatchJobs=new Map([["1",{"label":"Sonar Scan","amount":12,"priority":"normal","touches":0}],["2",{"label":"Hot Spring Sampling","amount":21,"priority":"urgent","touches":0}]]);let workflowDispatchSerial=workflowDispatchJobs.size;function workflowDispatchDrive(job,activate=false){workflowDispatchCore.dataset.dispatchPriority=job.priority;workflowDispatchCore.style.setProperty('--dispatch-load',Math.min(.95,.16+job.amount/120));const bridge=activate?workflowBridgeActivate():{changed:false,summary:workflowBridgeSummary()};document.querySelector('#workflowDispatchSummary').textContent=job.label+' · '+job.amount+' m · '+bridge.summary;announce('Dive mission current task:'+job.label);}function workflowDispatchRender(activeId,activate=false){workflowDispatchSelect.replaceChildren(...[...workflowDispatchJobs].map(([id,job])=>{const option=document.createElement('option');option.value=id;option.textContent=job.label+(job.priority==='urgent'?' · Urgent':'');return option;}));workflowDispatchSelect.value=activeId;workflowDispatchDrive(workflowDispatchJobs.get(activeId),activate);}workflowDispatchForm.addEventListener('submit',event=>{event.preventDefault();const data=new FormData(workflowDispatchForm);const id=String(++workflowDispatchSerial);workflowDispatchJobs.set(id,{label:document.querySelector('#workflowDispatchKind').value,amount:Math.max(1,Number(document.querySelector('#workflowDispatchAmount').value)||1),priority:data.get('workflowPriority'),touches:0});workflowDispatchRender(id,true);});workflowDispatchSelect.addEventListener('change',()=>workflowDispatchDrive(workflowDispatchJobs.get(workflowDispatchSelect.value),true));workflowDispatchCore.addEventListener('focusin',()=>{const job=workflowDispatchJobs.get(workflowDispatchSelect.value);if(!job)return;job.touches+=1;document.querySelector('#workflowDispatchSummary').textContent=job.label+' · '+job.amount+' m · '+workflowBridgeSummary();});workflowDispatchRender('1',false);
workflowBridgeReset();
  </script>
</body>
</html>
Page feedbackFound an issue or have suggestions for improvement?
Feedback features need to be used in the online version

Please enable JavaScript in the online version before submitting; local features of the current tool or example are not affected.

Go to the online version for feedback